agileflow 2.91.0 → 2.92.1
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 +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +32 -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 +491 -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 +50 -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 +127 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +408 -55
- package/scripts/spawn-parallel.js +666 -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 +132 -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 +95 -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/installers/ide/windsurf.js +1 -1
- 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 +113 -2
- package/tools/cli/lib/ui.js +15 -25
|
@@ -5,12 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
set -e
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
# Source shared utilities
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
|
|
11
|
+
# Source colors from canonical source (lib/colors.sh)
|
|
12
|
+
if [[ -f "$SCRIPT_DIR/lib/colors.sh" ]]; then
|
|
13
|
+
source "$SCRIPT_DIR/lib/colors.sh"
|
|
14
|
+
NC="$RESET" # Alias for backwards compatibility
|
|
15
|
+
else
|
|
16
|
+
# Fallback colors if colors.sh not available
|
|
17
|
+
RED='\033[0;31m'
|
|
18
|
+
GREEN='\033[0;32m'
|
|
19
|
+
YELLOW='\033[1;33m'
|
|
20
|
+
BLUE='\033[0;34m'
|
|
21
|
+
NC='\033[0m'
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Source JSON utilities if available
|
|
25
|
+
if [[ -f "$SCRIPT_DIR/lib/json-utils.sh" ]]; then
|
|
26
|
+
source "$SCRIPT_DIR/lib/json-utils.sh"
|
|
27
|
+
fi
|
|
14
28
|
|
|
15
29
|
# Default paths (relative to project root)
|
|
16
30
|
DOCS_DIR="docs"
|
|
@@ -45,12 +59,17 @@ THRESHOLD_DAYS=7
|
|
|
45
59
|
ENABLED=true
|
|
46
60
|
|
|
47
61
|
if [[ -f "$METADATA_FILE" ]]; then
|
|
48
|
-
if
|
|
62
|
+
# Use safeJsonParse if available (from json-utils.sh), otherwise fallback
|
|
63
|
+
if declare -f safeJsonParse > /dev/null; then
|
|
64
|
+
ENABLED=$(safeJsonParse "$METADATA_FILE" ".archival.enabled" "true")
|
|
65
|
+
THRESHOLD_DAYS=$(safeJsonParse "$METADATA_FILE" ".archival.threshold_days" "7")
|
|
66
|
+
elif command -v jq &> /dev/null; then
|
|
49
67
|
ENABLED=$(jq -r '.archival.enabled // true' "$METADATA_FILE")
|
|
50
68
|
THRESHOLD_DAYS=$(jq -r '.archival.threshold_days // 7' "$METADATA_FILE")
|
|
51
69
|
elif command -v node &> /dev/null; then
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
# Security: Pass file path via environment variable, not string interpolation
|
|
71
|
+
ENABLED=$(METADATA_PATH="$METADATA_FILE" node -pe "JSON.parse(require('fs').readFileSync(process.env.METADATA_PATH, 'utf8')).archival?.enabled ?? true" 2>/dev/null || echo "true")
|
|
72
|
+
THRESHOLD_DAYS=$(METADATA_PATH="$METADATA_FILE" node -pe "JSON.parse(require('fs').readFileSync(process.env.METADATA_PATH, 'utf8')).archival?.threshold_days ?? 7" 2>/dev/null || echo "7")
|
|
54
73
|
fi
|
|
55
74
|
fi
|
|
56
75
|
|
|
@@ -64,6 +83,15 @@ echo -e "${BLUE}Starting auto-archival (threshold: $THRESHOLD_DAYS days)...${NC}
|
|
|
64
83
|
# Create archive directory if needed
|
|
65
84
|
mkdir -p "$ARCHIVE_DIR"
|
|
66
85
|
|
|
86
|
+
# Security: Validate archive directory is not a symlink pointing outside project
|
|
87
|
+
if [[ -L "$ARCHIVE_DIR" ]]; then
|
|
88
|
+
RESOLVED_ARCHIVE=$(readlink -f "$ARCHIVE_DIR" 2>/dev/null || realpath "$ARCHIVE_DIR" 2>/dev/null)
|
|
89
|
+
if [[ ! "$RESOLVED_ARCHIVE" == "$PROJECT_ROOT"* ]]; then
|
|
90
|
+
echo -e "${RED}Error: Archive directory symlink points outside project. Aborting.${NC}"
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
|
|
67
95
|
# Calculate cutoff date (threshold days ago)
|
|
68
96
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
69
97
|
# macOS
|
|
@@ -118,6 +146,12 @@ for (const [storyId, story] of Object.entries(toArchive)) {
|
|
|
118
146
|
const date = new Date(story.completed_at);
|
|
119
147
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
120
148
|
|
|
149
|
+
// Security: Validate monthKey matches expected format (YYYY-MM) to prevent path traversal
|
|
150
|
+
if (!/^\d{4}-\d{2}$/.test(monthKey)) {
|
|
151
|
+
console.error(`\x1b[31mSkipping story ${storyId}: invalid date format\x1b[0m`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
121
155
|
if (!byMonth[monthKey]) {
|
|
122
156
|
byMonth[monthKey] = {
|
|
123
157
|
month: monthKey,
|
|
@@ -133,10 +167,22 @@ for (const [storyId, story] of Object.entries(toArchive)) {
|
|
|
133
167
|
for (const [monthKey, archiveData] of Object.entries(byMonth)) {
|
|
134
168
|
const archiveFile = path.join(archiveDir, `${monthKey}.json`);
|
|
135
169
|
|
|
170
|
+
// Security: Verify resolved path stays within archive directory
|
|
171
|
+
const resolvedPath = path.resolve(archiveFile);
|
|
172
|
+
const resolvedArchiveDir = path.resolve(archiveDir);
|
|
173
|
+
if (!resolvedPath.startsWith(resolvedArchiveDir + path.sep) && resolvedPath !== resolvedArchiveDir) {
|
|
174
|
+
console.error(`\x1b[31mSecurity: Archive path ${archiveFile} escapes archive directory. Skipping.\x1b[0m`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
136
178
|
// Merge with existing archive if it exists
|
|
137
179
|
if (fs.existsSync(archiveFile)) {
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
try {
|
|
181
|
+
const existing = JSON.parse(fs.readFileSync(archiveFile, 'utf8'));
|
|
182
|
+
archiveData.stories = { ...existing.stories, ...archiveData.stories };
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.error(`\x1b[31mWarning: Could not parse existing ${monthKey}.json, will overwrite\x1b[0m`);
|
|
185
|
+
}
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
fs.writeFileSync(archiveFile, JSON.stringify(archiveData, null, 2));
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# claude-tmux.sh - Wrapper script that auto-starts Claude Code in a tmux session
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./claude-tmux.sh # Start in tmux with default session
|
|
6
|
+
# ./claude-tmux.sh --no-tmux # Start without tmux (regular claude)
|
|
7
|
+
# ./claude-tmux.sh -n # Same as --no-tmux
|
|
8
|
+
#
|
|
9
|
+
# When already in tmux: Just runs claude normally
|
|
10
|
+
# When not in tmux: Creates a tmux session and runs claude inside it
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
# Check for --no-tmux flag
|
|
15
|
+
NO_TMUX=false
|
|
16
|
+
for arg in "$@"; do
|
|
17
|
+
case $arg in
|
|
18
|
+
--no-tmux|-n)
|
|
19
|
+
NO_TMUX=true
|
|
20
|
+
shift
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
# If --no-tmux was specified, just run claude directly
|
|
26
|
+
if [ "$NO_TMUX" = true ]; then
|
|
27
|
+
exec claude "$@"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Check if tmux auto-spawn is disabled in config
|
|
31
|
+
METADATA_FILE="docs/00-meta/agileflow-metadata.json"
|
|
32
|
+
if [ -f "$METADATA_FILE" ]; then
|
|
33
|
+
# Use node to parse JSON (more reliable than jq which may not be installed)
|
|
34
|
+
TMUX_ENABLED=$(node -e "
|
|
35
|
+
try {
|
|
36
|
+
const meta = JSON.parse(require('fs').readFileSync('$METADATA_FILE', 'utf8'));
|
|
37
|
+
// Default to true (enabled) if not explicitly set to false
|
|
38
|
+
console.log(meta.features?.tmuxAutoSpawn?.enabled !== false ? 'true' : 'false');
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.log('true'); // Default to enabled on error
|
|
41
|
+
}
|
|
42
|
+
" 2>/dev/null || echo "true")
|
|
43
|
+
|
|
44
|
+
if [ "$TMUX_ENABLED" = "false" ]; then
|
|
45
|
+
exec claude "$@"
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check if we're already inside tmux
|
|
50
|
+
if [ -n "$TMUX" ]; then
|
|
51
|
+
# Already in tmux, just run claude
|
|
52
|
+
exec claude "$@"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Check if tmux is available
|
|
56
|
+
if ! command -v tmux &> /dev/null; then
|
|
57
|
+
echo "tmux not found. Running claude without tmux."
|
|
58
|
+
echo "Install tmux for parallel session support:"
|
|
59
|
+
echo " macOS: brew install tmux"
|
|
60
|
+
echo " Ubuntu/Debian: sudo apt install tmux"
|
|
61
|
+
echo ""
|
|
62
|
+
exec claude "$@"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Generate session name based on current directory
|
|
66
|
+
DIR_NAME=$(basename "$(pwd)")
|
|
67
|
+
SESSION_NAME="claude-${DIR_NAME}"
|
|
68
|
+
|
|
69
|
+
# Check if session already exists
|
|
70
|
+
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
71
|
+
echo "Attaching to existing session: $SESSION_NAME"
|
|
72
|
+
exec tmux attach-session -t "$SESSION_NAME"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Create new tmux session with Claude
|
|
76
|
+
echo "Starting Claude in tmux session: $SESSION_NAME"
|
|
77
|
+
|
|
78
|
+
# Create session in detached mode first
|
|
79
|
+
tmux new-session -d -s "$SESSION_NAME" -n "main"
|
|
80
|
+
|
|
81
|
+
# Minimal config - mouse and scrolling only, no fancy styling
|
|
82
|
+
tmux set-option -t "$SESSION_NAME" mouse on
|
|
83
|
+
|
|
84
|
+
# Fix colors - proper terminal support
|
|
85
|
+
tmux set-option -t "$SESSION_NAME" default-terminal "xterm-256color"
|
|
86
|
+
|
|
87
|
+
# Keybindings: Alt+number to switch windows, Alt+q to detach
|
|
88
|
+
for i in 1 2 3 4 5 6 7 8 9; do
|
|
89
|
+
tmux bind-key -n "M-$i" select-window -t ":$((i-1))"
|
|
90
|
+
done
|
|
91
|
+
tmux bind-key -n M-q detach-client
|
|
92
|
+
|
|
93
|
+
# Send the claude command to the first window
|
|
94
|
+
CLAUDE_CMD="claude"
|
|
95
|
+
if [ $# -gt 0 ]; then
|
|
96
|
+
# Pass any remaining arguments to claude
|
|
97
|
+
CLAUDE_CMD="claude $*"
|
|
98
|
+
fi
|
|
99
|
+
tmux send-keys -t "$SESSION_NAME" "$CLAUDE_CMD" Enter
|
|
100
|
+
|
|
101
|
+
# Attach to the session
|
|
102
|
+
exec tmux attach-session -t "$SESSION_NAME"
|
|
@@ -15,74 +15,7 @@
|
|
|
15
15
|
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const {
|
|
19
|
-
findProjectRoot,
|
|
20
|
-
loadPatterns,
|
|
21
|
-
outputBlocked,
|
|
22
|
-
runDamageControlHook,
|
|
23
|
-
parseBashPatterns,
|
|
24
|
-
c,
|
|
25
|
-
} = require('./lib/damage-control-utils');
|
|
18
|
+
const { createBashHook } = require('./lib/damage-control-utils');
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
30
|
-
function matchesPattern(command, rule) {
|
|
31
|
-
try {
|
|
32
|
-
const flags = rule.flags || '';
|
|
33
|
-
const regex = new RegExp(rule.pattern, flags);
|
|
34
|
-
return regex.test(command);
|
|
35
|
-
} catch (e) {
|
|
36
|
-
// Invalid regex - skip this pattern
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Validate command against all patterns
|
|
43
|
-
*/
|
|
44
|
-
function validateCommand(command, config) {
|
|
45
|
-
// Check blocked patterns (bashToolPatterns + agileflowProtections)
|
|
46
|
-
const blockedPatterns = [
|
|
47
|
-
...(config.bashToolPatterns || []),
|
|
48
|
-
...(config.agileflowProtections || []),
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
for (const rule of blockedPatterns) {
|
|
52
|
-
if (matchesPattern(command, rule)) {
|
|
53
|
-
return {
|
|
54
|
-
action: 'block',
|
|
55
|
-
reason: rule.reason || 'Command blocked by damage control',
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check ask patterns
|
|
61
|
-
for (const rule of config.askPatterns || []) {
|
|
62
|
-
if (matchesPattern(command, rule)) {
|
|
63
|
-
return {
|
|
64
|
-
action: 'ask',
|
|
65
|
-
reason: rule.reason || 'Please confirm this command',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Allow by default
|
|
71
|
-
return { action: 'allow' };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Run the hook
|
|
75
|
-
const projectRoot = findProjectRoot();
|
|
76
|
-
const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
|
|
77
|
-
|
|
78
|
-
runDamageControlHook({
|
|
79
|
-
getInputValue: input => input.command || input.tool_input?.command,
|
|
80
|
-
loadConfig: () => loadPatterns(projectRoot, parseBashPatterns, defaultConfig),
|
|
81
|
-
validate: validateCommand,
|
|
82
|
-
onBlock: (result, command) => {
|
|
83
|
-
outputBlocked(
|
|
84
|
-
result.reason,
|
|
85
|
-
`Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`
|
|
86
|
-
);
|
|
87
|
-
},
|
|
88
|
-
});
|
|
20
|
+
// Run the hook using factory
|
|
21
|
+
createBashHook()();
|
|
@@ -12,24 +12,7 @@
|
|
|
12
12
|
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const {
|
|
16
|
-
findProjectRoot,
|
|
17
|
-
loadPatterns,
|
|
18
|
-
outputBlocked,
|
|
19
|
-
runDamageControlHook,
|
|
20
|
-
parsePathPatterns,
|
|
21
|
-
validatePathAgainstPatterns,
|
|
22
|
-
} = require('./lib/damage-control-utils');
|
|
15
|
+
const { createPathHook } = require('./lib/damage-control-utils');
|
|
23
16
|
|
|
24
|
-
// Run the hook
|
|
25
|
-
|
|
26
|
-
const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
27
|
-
|
|
28
|
-
runDamageControlHook({
|
|
29
|
-
getInputValue: input => input.file_path || input.tool_input?.file_path,
|
|
30
|
-
loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
|
|
31
|
-
validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'edit'),
|
|
32
|
-
onBlock: (result, filePath) => {
|
|
33
|
-
outputBlocked(result.reason, result.detail, `File: ${filePath}`);
|
|
34
|
-
},
|
|
35
|
-
});
|
|
17
|
+
// Run the hook using factory
|
|
18
|
+
createPathHook('edit')();
|
|
@@ -12,24 +12,7 @@
|
|
|
12
12
|
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const {
|
|
16
|
-
findProjectRoot,
|
|
17
|
-
loadPatterns,
|
|
18
|
-
outputBlocked,
|
|
19
|
-
runDamageControlHook,
|
|
20
|
-
parsePathPatterns,
|
|
21
|
-
validatePathAgainstPatterns,
|
|
22
|
-
} = require('./lib/damage-control-utils');
|
|
15
|
+
const { createPathHook } = require('./lib/damage-control-utils');
|
|
23
16
|
|
|
24
|
-
// Run the hook
|
|
25
|
-
|
|
26
|
-
const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
27
|
-
|
|
28
|
-
runDamageControlHook({
|
|
29
|
-
getInputValue: input => input.file_path || input.tool_input?.file_path,
|
|
30
|
-
loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
|
|
31
|
-
validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'write'),
|
|
32
|
-
onBlock: (result, filePath) => {
|
|
33
|
-
outputBlocked(result.reason, result.detail, `File: ${filePath}`);
|
|
34
|
-
},
|
|
35
|
-
});
|
|
17
|
+
// Run the hook using factory
|
|
18
|
+
createPathHook('write')();
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgileFlow CLI - Dependency Check Script
|
|
5
|
+
*
|
|
6
|
+
* Local script for checking dependency vulnerabilities and generating reports.
|
|
7
|
+
* Can be run manually or integrated into CI/CD pipelines.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/dependency-check.js [options]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --json Output results as JSON
|
|
14
|
+
* --fix Attempt to auto-fix vulnerabilities
|
|
15
|
+
* --force Force fixes (may include breaking changes)
|
|
16
|
+
* --severity=X Minimum severity to report (low, moderate, high, critical)
|
|
17
|
+
* --quiet Only show errors
|
|
18
|
+
* --help Show help
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { execSync } = require('child_process');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
// ANSI colors
|
|
26
|
+
const c = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
bold: '\x1b[1m',
|
|
29
|
+
dim: '\x1b[2m',
|
|
30
|
+
red: '\x1b[31m',
|
|
31
|
+
green: '\x1b[32m',
|
|
32
|
+
yellow: '\x1b[33m',
|
|
33
|
+
cyan: '\x1b[36m',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const SEVERITY_LEVELS = ['low', 'moderate', 'high', 'critical'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse command line arguments
|
|
40
|
+
*/
|
|
41
|
+
function parseArgs(args) {
|
|
42
|
+
const options = {
|
|
43
|
+
json: false,
|
|
44
|
+
fix: false,
|
|
45
|
+
force: false,
|
|
46
|
+
severity: 'low',
|
|
47
|
+
quiet: false,
|
|
48
|
+
help: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const arg of args) {
|
|
52
|
+
if (arg === '--json') options.json = true;
|
|
53
|
+
else if (arg === '--fix') options.fix = true;
|
|
54
|
+
else if (arg === '--force') options.force = true;
|
|
55
|
+
else if (arg === '--quiet') options.quiet = true;
|
|
56
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
57
|
+
else if (arg.startsWith('--severity=')) {
|
|
58
|
+
const level = arg.split('=')[1].toLowerCase();
|
|
59
|
+
if (SEVERITY_LEVELS.includes(level)) {
|
|
60
|
+
options.severity = level;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return options;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Show help message
|
|
70
|
+
*/
|
|
71
|
+
function showHelp() {
|
|
72
|
+
console.log(`
|
|
73
|
+
${c.bold}AgileFlow Dependency Check${c.reset}
|
|
74
|
+
|
|
75
|
+
${c.cyan}Usage:${c.reset}
|
|
76
|
+
node scripts/dependency-check.js [options]
|
|
77
|
+
|
|
78
|
+
${c.cyan}Options:${c.reset}
|
|
79
|
+
--json Output results as JSON
|
|
80
|
+
--fix Attempt to auto-fix vulnerabilities
|
|
81
|
+
--force Force fixes (may include breaking changes)
|
|
82
|
+
--severity=X Minimum severity to report (low, moderate, high, critical)
|
|
83
|
+
--quiet Only show errors
|
|
84
|
+
--help, -h Show this help message
|
|
85
|
+
|
|
86
|
+
${c.cyan}Examples:${c.reset}
|
|
87
|
+
# Check for all vulnerabilities
|
|
88
|
+
node scripts/dependency-check.js
|
|
89
|
+
|
|
90
|
+
# Only report high and critical
|
|
91
|
+
node scripts/dependency-check.js --severity=high
|
|
92
|
+
|
|
93
|
+
# Auto-fix and output JSON
|
|
94
|
+
node scripts/dependency-check.js --fix --json
|
|
95
|
+
|
|
96
|
+
# Force all fixes
|
|
97
|
+
node scripts/dependency-check.js --fix --force
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run npm audit and parse results
|
|
103
|
+
*/
|
|
104
|
+
function runAudit() {
|
|
105
|
+
try {
|
|
106
|
+
const output = execSync('npm audit --json 2>&1', {
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
|
|
109
|
+
});
|
|
110
|
+
return JSON.parse(output);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// npm audit exits with non-zero if vulnerabilities found
|
|
113
|
+
if (error.stdout) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(error.stdout);
|
|
116
|
+
} catch {
|
|
117
|
+
return { error: error.message, metadata: { vulnerabilities: {} } };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { error: error.message, metadata: { vulnerabilities: {} } };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply npm audit fix
|
|
126
|
+
*/
|
|
127
|
+
function runFix(force = false) {
|
|
128
|
+
const cmd = force
|
|
129
|
+
? 'npm audit fix --force --legacy-peer-deps'
|
|
130
|
+
: 'npm audit fix --legacy-peer-deps';
|
|
131
|
+
try {
|
|
132
|
+
const output = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' });
|
|
133
|
+
return { success: true, output };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { success: false, error: error.message, output: error.stdout || '' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Filter vulnerabilities by severity
|
|
141
|
+
*/
|
|
142
|
+
function filterBySeverity(audit, minSeverity) {
|
|
143
|
+
const minIndex = SEVERITY_LEVELS.indexOf(minSeverity);
|
|
144
|
+
const filtered = { ...audit };
|
|
145
|
+
|
|
146
|
+
if (audit.vulnerabilities) {
|
|
147
|
+
filtered.vulnerabilities = Object.fromEntries(
|
|
148
|
+
Object.entries(audit.vulnerabilities).filter(([, vuln]) => {
|
|
149
|
+
const vulnIndex = SEVERITY_LEVELS.indexOf(vuln.severity);
|
|
150
|
+
return vulnIndex >= minIndex;
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return filtered;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format audit results for console output
|
|
160
|
+
*/
|
|
161
|
+
function formatResults(audit, options) {
|
|
162
|
+
const { metadata = {}, vulnerabilities = {} } = audit;
|
|
163
|
+
const vulnCounts = metadata.vulnerabilities || {};
|
|
164
|
+
|
|
165
|
+
const lines = [];
|
|
166
|
+
|
|
167
|
+
// Header
|
|
168
|
+
lines.push(`\n${c.bold}Dependency Audit Report${c.reset}`);
|
|
169
|
+
lines.push(`${c.dim}${'─'.repeat(40)}${c.reset}\n`);
|
|
170
|
+
|
|
171
|
+
// Summary
|
|
172
|
+
const total = vulnCounts.total || 0;
|
|
173
|
+
if (total === 0) {
|
|
174
|
+
lines.push(`${c.green}✓ No vulnerabilities found!${c.reset}\n`);
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lines.push(`${c.bold}Vulnerabilities Found: ${total}${c.reset}`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
|
|
181
|
+
// By severity
|
|
182
|
+
if (vulnCounts.critical > 0) {
|
|
183
|
+
lines.push(` ${c.red}● Critical: ${vulnCounts.critical}${c.reset}`);
|
|
184
|
+
}
|
|
185
|
+
if (vulnCounts.high > 0) {
|
|
186
|
+
lines.push(` ${c.yellow}● High: ${vulnCounts.high}${c.reset}`);
|
|
187
|
+
}
|
|
188
|
+
if (vulnCounts.moderate > 0) {
|
|
189
|
+
lines.push(` ${c.cyan}● Moderate: ${vulnCounts.moderate}${c.reset}`);
|
|
190
|
+
}
|
|
191
|
+
if (vulnCounts.low > 0) {
|
|
192
|
+
lines.push(` ${c.dim}● Low: ${vulnCounts.low}${c.reset}`);
|
|
193
|
+
}
|
|
194
|
+
lines.push('');
|
|
195
|
+
|
|
196
|
+
// Details (if not quiet)
|
|
197
|
+
if (!options.quiet && Object.keys(vulnerabilities).length > 0) {
|
|
198
|
+
lines.push(`${c.bold}Details:${c.reset}`);
|
|
199
|
+
for (const [name, vuln] of Object.entries(vulnerabilities)) {
|
|
200
|
+
const severityColor =
|
|
201
|
+
vuln.severity === 'critical'
|
|
202
|
+
? c.red
|
|
203
|
+
: vuln.severity === 'high'
|
|
204
|
+
? c.yellow
|
|
205
|
+
: vuln.severity === 'moderate'
|
|
206
|
+
? c.cyan
|
|
207
|
+
: c.dim;
|
|
208
|
+
lines.push(` ${severityColor}[${vuln.severity.toUpperCase()}]${c.reset} ${name}`);
|
|
209
|
+
if (vuln.via && Array.isArray(vuln.via)) {
|
|
210
|
+
const vias = vuln.via.filter(v => typeof v === 'object').slice(0, 2);
|
|
211
|
+
for (const via of vias) {
|
|
212
|
+
if (via.title) {
|
|
213
|
+
lines.push(` └─ ${via.title}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Recommendations
|
|
222
|
+
lines.push(`${c.bold}Recommendations:${c.reset}`);
|
|
223
|
+
lines.push(' Run: npm audit fix');
|
|
224
|
+
if (vulnCounts.critical > 0 || vulnCounts.high > 0) {
|
|
225
|
+
lines.push(' Or force: npm audit fix --force (may include breaking changes)');
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Main function
|
|
234
|
+
*/
|
|
235
|
+
async function main() {
|
|
236
|
+
const options = parseArgs(process.argv.slice(2));
|
|
237
|
+
|
|
238
|
+
if (options.help) {
|
|
239
|
+
showHelp();
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!options.quiet && !options.json) {
|
|
244
|
+
console.log(`${c.cyan}Running dependency audit...${c.reset}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Run audit
|
|
248
|
+
let audit = runAudit();
|
|
249
|
+
|
|
250
|
+
// Filter by severity
|
|
251
|
+
audit = filterBySeverity(audit, options.severity);
|
|
252
|
+
|
|
253
|
+
// Apply fix if requested
|
|
254
|
+
let fixResult = null;
|
|
255
|
+
if (options.fix) {
|
|
256
|
+
if (!options.quiet && !options.json) {
|
|
257
|
+
console.log(`${c.cyan}Applying fixes...${c.reset}`);
|
|
258
|
+
}
|
|
259
|
+
fixResult = runFix(options.force);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Output results
|
|
263
|
+
if (options.json) {
|
|
264
|
+
const result = {
|
|
265
|
+
audit,
|
|
266
|
+
fix: fixResult,
|
|
267
|
+
timestamp: new Date().toISOString(),
|
|
268
|
+
severity_filter: options.severity,
|
|
269
|
+
};
|
|
270
|
+
console.log(JSON.stringify(result, null, 2));
|
|
271
|
+
} else {
|
|
272
|
+
console.log(formatResults(audit, options));
|
|
273
|
+
if (fixResult) {
|
|
274
|
+
if (fixResult.success) {
|
|
275
|
+
console.log(`${c.green}✓ Fixes applied successfully${c.reset}`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log(`${c.yellow}⚠ Some fixes could not be applied${c.reset}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Exit with appropriate code
|
|
283
|
+
const vulnCounts = audit.metadata?.vulnerabilities || {};
|
|
284
|
+
const total = vulnCounts.total || 0;
|
|
285
|
+
const hasHighSeverity = (vulnCounts.critical || 0) + (vulnCounts.high || 0) > 0;
|
|
286
|
+
|
|
287
|
+
if (hasHighSeverity) {
|
|
288
|
+
process.exit(2); // High/critical vulnerabilities
|
|
289
|
+
} else if (total > 0) {
|
|
290
|
+
process.exit(1); // Some vulnerabilities
|
|
291
|
+
}
|
|
292
|
+
process.exit(0); // All clear
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Run if called directly
|
|
296
|
+
if (require.main === module) {
|
|
297
|
+
main().catch(error => {
|
|
298
|
+
console.error(`${c.red}Error: ${error.message}${c.reset}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
parseArgs,
|
|
305
|
+
runAudit,
|
|
306
|
+
runFix,
|
|
307
|
+
filterBySeverity,
|
|
308
|
+
formatResults,
|
|
309
|
+
SEVERITY_LEVELS,
|
|
310
|
+
};
|
package/scripts/get-env.js
CHANGED
|
@@ -19,6 +19,12 @@ const path = require('path');
|
|
|
19
19
|
const os = require('os');
|
|
20
20
|
const { execSync } = require('child_process');
|
|
21
21
|
|
|
22
|
+
// Import centralized path utilities
|
|
23
|
+
const { getStatusPath } = require('../lib/paths');
|
|
24
|
+
|
|
25
|
+
// Import centralized file cache (US-0176: deduplicated status.json access)
|
|
26
|
+
const { readJSONCached } = require('../lib/file-cache');
|
|
27
|
+
|
|
22
28
|
function getProjectInfo() {
|
|
23
29
|
const rootDir = path.resolve(__dirname, '..');
|
|
24
30
|
|
|
@@ -72,14 +78,15 @@ function getProjectInfo() {
|
|
|
72
78
|
const activeEpics = [];
|
|
73
79
|
|
|
74
80
|
try {
|
|
75
|
-
const statusPath =
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const statusPath = getStatusPath(rootDir);
|
|
82
|
+
// Use centralized file cache (US-0176: 60s TTL, shared across all consumers)
|
|
83
|
+
const status = readJSONCached(statusPath, { ttlMs: 60000 });
|
|
78
84
|
|
|
85
|
+
if (status) {
|
|
79
86
|
// Get active stories
|
|
80
87
|
if (status.stories) {
|
|
81
88
|
Object.entries(status.stories).forEach(([id, story]) => {
|
|
82
|
-
if (story.status === '
|
|
89
|
+
if (story.status === 'in-progress') {
|
|
83
90
|
activeStories.push({ id, title: story.title, owner: story.owner });
|
|
84
91
|
wipCount++;
|
|
85
92
|
}
|