agileflow 2.80.0 → 2.82.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.
@@ -29,6 +29,7 @@
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
31
  const { execSync } = require('child_process');
32
+ const { isValidProfileName, isValidFeatureName, parseIntBounded } = require('../lib/validate');
32
33
 
33
34
  // ============================================================================
34
35
  // CONFIGURATION
@@ -1623,7 +1624,8 @@ function main() {
1623
1624
  .split('=')[1]
1624
1625
  .split(',')
1625
1626
  .map(s => s.trim().toLowerCase());
1626
- else if (arg.startsWith('--archival-days=')) archivalDays = parseInt(arg.split('=')[1]) || 30;
1627
+ else if (arg.startsWith('--archival-days='))
1628
+ archivalDays = parseIntBounded(arg.split('=')[1], 30, 1, 365);
1627
1629
  else if (arg === '--migrate') migrate = true;
1628
1630
  else if (arg === '--detect' || arg === '--validate') detect = true;
1629
1631
  else if (arg === '--upgrade') upgrade = true;
@@ -207,6 +207,12 @@ function checkParallelSessions(rootDir) {
207
207
  otherActive: 0,
208
208
  currentId: null,
209
209
  cleaned: 0,
210
+ // Extended session info for non-main sessions
211
+ isMain: true,
212
+ nickname: null,
213
+ branch: null,
214
+ sessionPath: null,
215
+ mainPath: rootDir,
210
216
  };
211
217
 
212
218
  try {
@@ -247,6 +253,24 @@ function checkParallelSessions(rootDir) {
247
253
  } catch (e) {
248
254
  // Count failed
249
255
  }
256
+
257
+ // Get detailed status for current session (for banner display)
258
+ try {
259
+ const statusOutput = execSync(`node "${scriptPath}" status`, {
260
+ cwd: rootDir,
261
+ encoding: 'utf8',
262
+ stdio: ['pipe', 'pipe', 'pipe'],
263
+ });
264
+ const statusData = JSON.parse(statusOutput);
265
+ if (statusData.current) {
266
+ result.isMain = statusData.current.is_main === true;
267
+ result.nickname = statusData.current.nickname;
268
+ result.branch = statusData.current.branch;
269
+ result.sessionPath = statusData.current.path;
270
+ }
271
+ } catch (e) {
272
+ // Status failed
273
+ }
250
274
  } catch (e) {
251
275
  // Session system not available
252
276
  }
@@ -849,6 +873,41 @@ function formatTable(
849
873
  return lines.join('\n');
850
874
  }
851
875
 
876
+ // Format session banner for non-main sessions
877
+ function formatSessionBanner(parallelSessions) {
878
+ if (!parallelSessions.available || parallelSessions.isMain) {
879
+ return null;
880
+ }
881
+
882
+ const W = 62; // banner width
883
+ const lines = [];
884
+
885
+ // Get display name
886
+ const sessionName = parallelSessions.nickname
887
+ ? `SESSION ${parallelSessions.currentId} "${parallelSessions.nickname}"`
888
+ : `SESSION ${parallelSessions.currentId}`;
889
+
890
+ lines.push(`${c.dim}${box.tl}${box.h.repeat(W)}${box.tr}${c.reset}`);
891
+ lines.push(
892
+ `${c.dim}${box.v}${c.reset} ${c.teal}${c.bold}${pad(sessionName, W - 2)}${c.reset} ${c.dim}${box.v}${c.reset}`
893
+ );
894
+ lines.push(
895
+ `${c.dim}${box.v}${c.reset} ${c.slate}Branch:${c.reset} ${pad(parallelSessions.branch || 'unknown', W - 13)} ${c.dim}${box.v}${c.reset}`
896
+ );
897
+
898
+ // Show relative path to main
899
+ if (parallelSessions.sessionPath) {
900
+ const relPath = path.relative(parallelSessions.sessionPath, parallelSessions.mainPath) || '.';
901
+ lines.push(
902
+ `${c.dim}${box.v}${c.reset} ${c.slate}Main at:${c.reset} ${pad(relPath, W - 14)} ${c.dim}${box.v}${c.reset}`
903
+ );
904
+ }
905
+
906
+ lines.push(`${c.dim}${box.bl}${box.h.repeat(W)}${box.br}${c.reset}`);
907
+
908
+ return lines.join('\n');
909
+ }
910
+
852
911
  // Main
853
912
  async function main() {
854
913
  const rootDir = getProjectRoot();
@@ -882,6 +941,12 @@ async function main() {
882
941
  // Update check failed - continue without it
883
942
  }
884
943
 
944
+ // Show session banner FIRST if in a non-main session
945
+ const sessionBanner = formatSessionBanner(parallelSessions);
946
+ if (sessionBanner) {
947
+ console.log(sessionBanner);
948
+ }
949
+
885
950
  console.log(
886
951
  formatTable(
887
952
  info,
@@ -15,23 +15,13 @@
15
15
  * Usage: Configured as PreToolUse hook in .claude/settings.json
16
16
  */
17
17
 
18
- const fs = require('fs');
19
- const path = require('path');
20
- const { c } = require('../lib/colors');
21
-
22
- /**
23
- * Find project root by looking for .agileflow directory
24
- */
25
- function findProjectRoot() {
26
- let dir = process.cwd();
27
- while (dir !== '/') {
28
- if (fs.existsSync(path.join(dir, '.agileflow'))) {
29
- return dir;
30
- }
31
- dir = path.dirname(dir);
32
- }
33
- return process.cwd();
34
- }
18
+ const {
19
+ findProjectRoot,
20
+ loadPatterns,
21
+ outputBlocked,
22
+ runDamageControlHook,
23
+ c,
24
+ } = require('./lib/damage-control-utils');
35
25
 
36
26
  /**
37
27
  * Parse simplified YAML for damage control patterns
@@ -91,31 +81,6 @@ function parseSimpleYAML(content) {
91
81
  return config;
92
82
  }
93
83
 
94
- /**
95
- * Load patterns configuration from YAML file
96
- */
97
- function loadPatterns(projectRoot) {
98
- const configPaths = [
99
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
100
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
101
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
102
- ];
103
-
104
- for (const configPath of configPaths) {
105
- if (fs.existsSync(configPath)) {
106
- try {
107
- const content = fs.readFileSync(configPath, 'utf8');
108
- return parseSimpleYAML(content);
109
- } catch (e) {
110
- // Continue to next path
111
- }
112
- }
113
- }
114
-
115
- // Return empty config if no file found (fail-open)
116
- return { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
117
- }
118
-
119
84
  /**
120
85
  * Test command against a single pattern rule
121
86
  */
@@ -163,76 +128,18 @@ function validateCommand(command, config) {
163
128
  return { action: 'allow' };
164
129
  }
165
130
 
166
- /**
167
- * Main function - read input and validate
168
- */
169
- function main() {
170
- const projectRoot = findProjectRoot();
171
- let inputData = '';
172
-
173
- process.stdin.setEncoding('utf8');
174
-
175
- process.stdin.on('data', chunk => {
176
- inputData += chunk;
177
- });
178
-
179
- process.stdin.on('end', () => {
180
- try {
181
- // Parse tool input from Claude Code
182
- const input = JSON.parse(inputData);
183
- const command = input.command || input.tool_input?.command || '';
184
-
185
- if (!command) {
186
- // No command to validate - allow
187
- process.exit(0);
188
- }
189
-
190
- // Load patterns and validate
191
- const config = loadPatterns(projectRoot);
192
- const result = validateCommand(command, config);
193
-
194
- switch (result.action) {
195
- case 'block':
196
- // Output error message and block
197
- console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
198
- console.error(
199
- `${c.dim}Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}${c.reset}`
200
- );
201
- process.exit(2);
202
- break;
203
-
204
- case 'ask':
205
- // Output JSON to trigger user confirmation
206
- console.log(
207
- JSON.stringify({
208
- result: 'ask',
209
- message: result.reason,
210
- })
211
- );
212
- process.exit(0);
213
- break;
214
-
215
- case 'allow':
216
- default:
217
- // Allow command to proceed
218
- process.exit(0);
219
- }
220
- } catch (e) {
221
- // Parse error or other issue - fail open
222
- // This ensures broken config doesn't block all commands
223
- process.exit(0);
224
- }
225
- });
226
-
227
- // Handle no stdin (direct invocation)
228
- process.stdin.on('error', () => {
229
- process.exit(0);
230
- });
231
-
232
- // Set timeout to prevent hanging
233
- setTimeout(() => {
234
- process.exit(0);
235
- }, 4000);
236
- }
237
-
238
- main();
131
+ // Run the hook
132
+ const projectRoot = findProjectRoot();
133
+ const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
134
+
135
+ runDamageControlHook({
136
+ getInputValue: input => input.command || input.tool_input?.command,
137
+ loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
138
+ validate: validateCommand,
139
+ onBlock: (result, command) => {
140
+ outputBlocked(
141
+ result.reason,
142
+ `Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`
143
+ );
144
+ },
145
+ });
@@ -12,34 +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
- const { c } = require('../lib/colors');
19
-
20
- /**
21
- * Find project root by looking for .agileflow directory
22
- */
23
- function findProjectRoot() {
24
- let dir = process.cwd();
25
- while (dir !== '/') {
26
- if (fs.existsSync(path.join(dir, '.agileflow'))) {
27
- return dir;
28
- }
29
- dir = path.dirname(dir);
30
- }
31
- return process.cwd();
32
- }
33
-
34
- /**
35
- * Expand ~ to home directory
36
- */
37
- function expandPath(p) {
38
- if (p.startsWith('~/')) {
39
- return path.join(os.homedir(), p.slice(2));
40
- }
41
- return p;
42
- }
15
+ const {
16
+ findProjectRoot,
17
+ loadPatterns,
18
+ pathMatches,
19
+ outputBlocked,
20
+ runDamageControlHook,
21
+ } = require('./lib/damage-control-utils');
43
22
 
44
23
  /**
45
24
  * Parse simplified YAML for path patterns
@@ -79,79 +58,6 @@ function parseSimpleYAML(content) {
79
58
  return config;
80
59
  }
81
60
 
82
- /**
83
- * Load patterns configuration from YAML file
84
- */
85
- function loadPatterns(projectRoot) {
86
- const configPaths = [
87
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
88
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
89
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
90
- ];
91
-
92
- for (const configPath of configPaths) {
93
- if (fs.existsSync(configPath)) {
94
- try {
95
- const content = fs.readFileSync(configPath, 'utf8');
96
- return parseSimpleYAML(content);
97
- } catch (e) {
98
- // Continue to next path
99
- }
100
- }
101
- }
102
-
103
- // Return empty config if no file found (fail-open)
104
- return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
105
- }
106
-
107
- /**
108
- * Check if a file path matches any of the protected patterns
109
- */
110
- function pathMatches(filePath, patterns) {
111
- if (!filePath) return null;
112
-
113
- const normalizedPath = path.resolve(filePath);
114
- const relativePath = path.relative(process.cwd(), normalizedPath);
115
-
116
- for (const pattern of patterns) {
117
- const expandedPattern = expandPath(pattern);
118
-
119
- // Check if pattern is a directory prefix
120
- if (pattern.endsWith('/')) {
121
- const patternDir = expandedPattern.slice(0, -1);
122
- if (normalizedPath.startsWith(patternDir)) {
123
- return pattern;
124
- }
125
- }
126
-
127
- // Check exact match
128
- if (normalizedPath === expandedPattern) {
129
- return pattern;
130
- }
131
-
132
- // Check if normalized path ends with pattern (for filenames like "id_rsa")
133
- if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
134
- return pattern;
135
- }
136
-
137
- // Check if pattern appears in path (for patterns like "*.pem")
138
- if (pattern.startsWith('*')) {
139
- const ext = pattern.slice(1);
140
- if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
141
- return pattern;
142
- }
143
- }
144
-
145
- // Check if path contains pattern (for things like ".env.production")
146
- const patternBase = path.basename(pattern);
147
- if (path.basename(normalizedPath) === patternBase) {
148
- return pattern;
149
- }
150
- }
151
-
152
- return null;
153
- }
154
-
155
61
  /**
156
62
  * Validate file path for edit operation
157
63
  */
@@ -180,58 +86,15 @@ function validatePath(filePath, config) {
180
86
  return { action: 'allow' };
181
87
  }
182
88
 
183
- /**
184
- * Main function - read input and validate
185
- */
186
- function main() {
187
- const projectRoot = findProjectRoot();
188
- let inputData = '';
189
-
190
- process.stdin.setEncoding('utf8');
191
-
192
- process.stdin.on('data', chunk => {
193
- inputData += chunk;
194
- });
195
-
196
- process.stdin.on('end', () => {
197
- try {
198
- // Parse tool input from Claude Code
199
- const input = JSON.parse(inputData);
200
- const filePath = input.file_path || input.tool_input?.file_path || '';
201
-
202
- if (!filePath) {
203
- // No path to validate - allow
204
- process.exit(0);
205
- }
206
-
207
- // Load patterns and validate
208
- const config = loadPatterns(projectRoot);
209
- const result = validatePath(filePath, config);
210
-
211
- if (result.action === 'block') {
212
- console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
213
- console.error(`${c.dim}${result.detail}${c.reset}`);
214
- console.error(`${c.dim}File: ${filePath}${c.reset}`);
215
- process.exit(2);
216
- }
217
-
218
- // Allow
219
- process.exit(0);
220
- } catch (e) {
221
- // Parse error or other issue - fail open
222
- process.exit(0);
223
- }
224
- });
225
-
226
- // Handle no stdin
227
- process.stdin.on('error', () => {
228
- process.exit(0);
229
- });
230
-
231
- // Set timeout to prevent hanging
232
- setTimeout(() => {
233
- process.exit(0);
234
- }, 4000);
235
- }
236
-
237
- 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
+ });
@@ -12,34 +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
- const { c } = require('../lib/colors');
19
-
20
- /**
21
- * Find project root by looking for .agileflow directory
22
- */
23
- function findProjectRoot() {
24
- let dir = process.cwd();
25
- while (dir !== '/') {
26
- if (fs.existsSync(path.join(dir, '.agileflow'))) {
27
- return dir;
28
- }
29
- dir = path.dirname(dir);
30
- }
31
- return process.cwd();
32
- }
33
-
34
- /**
35
- * Expand ~ to home directory
36
- */
37
- function expandPath(p) {
38
- if (p.startsWith('~/')) {
39
- return path.join(os.homedir(), p.slice(2));
40
- }
41
- return p;
42
- }
15
+ const {
16
+ findProjectRoot,
17
+ loadPatterns,
18
+ pathMatches,
19
+ outputBlocked,
20
+ runDamageControlHook,
21
+ } = require('./lib/damage-control-utils');
43
22
 
44
23
  /**
45
24
  * Parse simplified YAML for path patterns
@@ -79,79 +58,6 @@ function parseSimpleYAML(content) {
79
58
  return config;
80
59
  }
81
60
 
82
- /**
83
- * Load patterns configuration from YAML file
84
- */
85
- function loadPatterns(projectRoot) {
86
- const configPaths = [
87
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
88
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
89
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
90
- ];
91
-
92
- for (const configPath of configPaths) {
93
- if (fs.existsSync(configPath)) {
94
- try {
95
- const content = fs.readFileSync(configPath, 'utf8');
96
- return parseSimpleYAML(content);
97
- } catch (e) {
98
- // Continue to next path
99
- }
100
- }
101
- }
102
-
103
- // Return empty config if no file found (fail-open)
104
- return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
105
- }
106
-
107
- /**
108
- * Check if a file path matches any of the protected patterns
109
- */
110
- function pathMatches(filePath, patterns) {
111
- if (!filePath) return null;
112
-
113
- const normalizedPath = path.resolve(filePath);
114
- const relativePath = path.relative(process.cwd(), normalizedPath);
115
-
116
- for (const pattern of patterns) {
117
- const expandedPattern = expandPath(pattern);
118
-
119
- // Check if pattern is a directory prefix
120
- if (pattern.endsWith('/')) {
121
- const patternDir = expandedPattern.slice(0, -1);
122
- if (normalizedPath.startsWith(patternDir)) {
123
- return pattern;
124
- }
125
- }
126
-
127
- // Check exact match
128
- if (normalizedPath === expandedPattern) {
129
- return pattern;
130
- }
131
-
132
- // Check if normalized path ends with pattern (for filenames like "id_rsa")
133
- if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
134
- return pattern;
135
- }
136
-
137
- // Check if pattern appears in path (for patterns like "*.pem")
138
- if (pattern.startsWith('*')) {
139
- const ext = pattern.slice(1);
140
- if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
141
- return pattern;
142
- }
143
- }
144
-
145
- // Check if path contains pattern (for things like ".env.production")
146
- const patternBase = path.basename(pattern);
147
- if (path.basename(normalizedPath) === patternBase) {
148
- return pattern;
149
- }
150
- }
151
-
152
- return null;
153
- }
154
-
155
61
  /**
156
62
  * Validate file path for write operation
157
63
  */
@@ -180,58 +86,15 @@ function validatePath(filePath, config) {
180
86
  return { action: 'allow' };
181
87
  }
182
88
 
183
- /**
184
- * Main function - read input and validate
185
- */
186
- function main() {
187
- const projectRoot = findProjectRoot();
188
- let inputData = '';
189
-
190
- process.stdin.setEncoding('utf8');
191
-
192
- process.stdin.on('data', chunk => {
193
- inputData += chunk;
194
- });
195
-
196
- process.stdin.on('end', () => {
197
- try {
198
- // Parse tool input from Claude Code
199
- const input = JSON.parse(inputData);
200
- const filePath = input.file_path || input.tool_input?.file_path || '';
201
-
202
- if (!filePath) {
203
- // No path to validate - allow
204
- process.exit(0);
205
- }
206
-
207
- // Load patterns and validate
208
- const config = loadPatterns(projectRoot);
209
- const result = validatePath(filePath, config);
210
-
211
- if (result.action === 'block') {
212
- console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
213
- console.error(`${c.dim}${result.detail}${c.reset}`);
214
- console.error(`${c.dim}File: ${filePath}${c.reset}`);
215
- process.exit(2);
216
- }
217
-
218
- // Allow
219
- process.exit(0);
220
- } catch (e) {
221
- // Parse error or other issue - fail open
222
- process.exit(0);
223
- }
224
- });
225
-
226
- // Handle no stdin
227
- process.stdin.on('error', () => {
228
- process.exit(0);
229
- });
230
-
231
- // Set timeout to prevent hanging
232
- setTimeout(() => {
233
- process.exit(0);
234
- }, 4000);
235
- }
236
-
237
- 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
+ });