wogiflow 1.0.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Security Utilities
|
|
5
|
+
*
|
|
6
|
+
* Shared security functions for safe command execution and path validation.
|
|
7
|
+
* Part of Phase 1: Critical Security Fixes (wf-9bcb4fa8)
|
|
8
|
+
*
|
|
9
|
+
* Functions:
|
|
10
|
+
* - validatePathWithinProject: Prevent path traversal attacks
|
|
11
|
+
* - safeExecFile: Execute commands safely without shell injection
|
|
12
|
+
* - safeGitCommand: Execute git commands with validated arguments
|
|
13
|
+
* - escapeRegex: Escape regex special characters for safe patterns
|
|
14
|
+
* - validateGitRef: Validate git branch/tag names
|
|
15
|
+
* - validateRepoFormat: Validate GitHub repository format
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// Constants
|
|
24
|
+
// ============================================================
|
|
25
|
+
|
|
26
|
+
/** Maximum length for regex patterns to prevent ReDoS */
|
|
27
|
+
const MAX_REGEX_LENGTH = 100;
|
|
28
|
+
|
|
29
|
+
/** Allowed characters in git branch names (per git-check-ref-format) */
|
|
30
|
+
const GIT_REF_PATTERN = /^[a-zA-Z0-9_\-./]+$/;
|
|
31
|
+
|
|
32
|
+
/** GitHub repository format: owner/repo */
|
|
33
|
+
const GITHUB_REPO_PATTERN = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/;
|
|
34
|
+
|
|
35
|
+
/** Valid file extensions for code search */
|
|
36
|
+
const VALID_CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
37
|
+
|
|
38
|
+
// ============================================================
|
|
39
|
+
// Path Validation
|
|
40
|
+
// ============================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate that a path is within the project root directory.
|
|
44
|
+
* Prevents path traversal attacks using ../ or absolute paths.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} filePath - Path to validate
|
|
47
|
+
* @param {string} projectRoot - Project root directory
|
|
48
|
+
* @returns {boolean} True if path is safely within project root
|
|
49
|
+
*/
|
|
50
|
+
function validatePathWithinProject(filePath, projectRoot) {
|
|
51
|
+
if (!filePath || !projectRoot) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Resolve to absolute path
|
|
56
|
+
const resolvedPath = path.resolve(projectRoot, filePath);
|
|
57
|
+
|
|
58
|
+
// Get real path if it exists (resolves symlinks)
|
|
59
|
+
let realPath = resolvedPath;
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(resolvedPath)) {
|
|
62
|
+
realPath = fs.realpathSync(resolvedPath);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// If realpathSync fails, continue with resolved path
|
|
66
|
+
if (process.env.DEBUG) console.warn(`[Security] realpathSync failed for ${resolvedPath}: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get real project root (resolves symlinks)
|
|
70
|
+
let realProjectRoot = projectRoot;
|
|
71
|
+
try {
|
|
72
|
+
realProjectRoot = fs.realpathSync(projectRoot);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// If realpathSync fails, continue with original
|
|
75
|
+
if (process.env.DEBUG) console.warn(`[Security] realpathSync failed for projectRoot: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Ensure path starts with project root + separator
|
|
79
|
+
// The separator check prevents /project/foo matching /project-other/foo
|
|
80
|
+
return realPath === realProjectRoot ||
|
|
81
|
+
realPath.startsWith(realProjectRoot + path.sep);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sanitize a path for safe use in file operations.
|
|
86
|
+
* Returns null if path is invalid or traverses outside project.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} filePath - Path to sanitize
|
|
89
|
+
* @param {string} projectRoot - Project root directory
|
|
90
|
+
* @returns {string|null} Sanitized absolute path or null if invalid
|
|
91
|
+
*/
|
|
92
|
+
function sanitizePath(filePath, projectRoot) {
|
|
93
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resolve path relative to project root
|
|
98
|
+
const resolved = path.resolve(projectRoot, filePath);
|
|
99
|
+
|
|
100
|
+
// Validate it's within project
|
|
101
|
+
if (!validatePathWithinProject(resolved, projectRoot)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================
|
|
109
|
+
// Safe Command Execution
|
|
110
|
+
// ============================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Execute a command safely using execFileSync (no shell).
|
|
114
|
+
* Prevents command injection by passing arguments as an array.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} command - Command to execute (e.g., 'git', 'grep')
|
|
117
|
+
* @param {string[]} args - Array of command arguments
|
|
118
|
+
* @param {Object} [options] - execFileSync options
|
|
119
|
+
* @param {string} [options.cwd] - Working directory
|
|
120
|
+
* @param {number} [options.timeout] - Timeout in milliseconds
|
|
121
|
+
* @param {string} [options.encoding] - Output encoding (default: 'utf-8')
|
|
122
|
+
* @returns {string} Command output
|
|
123
|
+
* @throws {Error} If command fails
|
|
124
|
+
*/
|
|
125
|
+
function safeExecFile(command, args, options = {}) {
|
|
126
|
+
const {
|
|
127
|
+
cwd = process.cwd(),
|
|
128
|
+
timeout = 30000,
|
|
129
|
+
encoding = 'utf-8',
|
|
130
|
+
...rest
|
|
131
|
+
} = options;
|
|
132
|
+
|
|
133
|
+
return execFileSync(command, args, {
|
|
134
|
+
cwd,
|
|
135
|
+
timeout,
|
|
136
|
+
encoding,
|
|
137
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
138
|
+
...rest
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Execute a command safely using spawnSync (no shell).
|
|
144
|
+
* Useful for commands that need more control over output handling.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} command - Command to execute
|
|
147
|
+
* @param {string[]} args - Array of command arguments
|
|
148
|
+
* @param {Object} [options] - spawnSync options
|
|
149
|
+
* @returns {Object} spawnSync result { status, stdout, stderr, error }
|
|
150
|
+
*/
|
|
151
|
+
function safeSpawn(command, args, options = {}) {
|
|
152
|
+
const {
|
|
153
|
+
cwd = process.cwd(),
|
|
154
|
+
timeout = 30000,
|
|
155
|
+
encoding = 'utf-8',
|
|
156
|
+
...rest
|
|
157
|
+
} = options;
|
|
158
|
+
|
|
159
|
+
return spawnSync(command, args, {
|
|
160
|
+
cwd,
|
|
161
|
+
timeout,
|
|
162
|
+
encoding,
|
|
163
|
+
...rest
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// Git Command Safety
|
|
169
|
+
// ============================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate a git reference name (branch, tag, commit).
|
|
173
|
+
* Based on git-check-ref-format rules.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} ref - Git reference to validate
|
|
176
|
+
* @returns {boolean} True if valid git reference
|
|
177
|
+
*/
|
|
178
|
+
function validateGitRef(ref) {
|
|
179
|
+
if (!ref || typeof ref !== 'string') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Length check
|
|
184
|
+
if (ref.length === 0 || ref.length > 255) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Cannot start or end with dot, cannot contain ..
|
|
189
|
+
if (ref.startsWith('.') || ref.endsWith('.') || ref.includes('..')) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Cannot contain certain characters
|
|
194
|
+
if (ref.includes(' ') || ref.includes('~') || ref.includes('^') ||
|
|
195
|
+
ref.includes(':') || ref.includes('?') || ref.includes('*') ||
|
|
196
|
+
ref.includes('[') || ref.includes('\\') || ref.includes('@{')) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Must match allowed pattern
|
|
201
|
+
return GIT_REF_PATTERN.test(ref);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Execute a git command with validated arguments.
|
|
206
|
+
* Prevents injection through git arguments.
|
|
207
|
+
*
|
|
208
|
+
* @param {string[]} args - Git subcommand and arguments as array
|
|
209
|
+
* @param {Object} [options] - Execution options
|
|
210
|
+
* @param {string} [options.cwd] - Working directory
|
|
211
|
+
* @param {boolean} [options.silent] - Suppress errors
|
|
212
|
+
* @returns {string|null} Command output or null if failed with silent=true
|
|
213
|
+
*/
|
|
214
|
+
function safeGitCommand(args, options = {}) {
|
|
215
|
+
const { cwd = process.cwd(), silent = false, timeout = 30000 } = options;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const result = execFileSync('git', args, {
|
|
219
|
+
cwd,
|
|
220
|
+
encoding: 'utf-8',
|
|
221
|
+
timeout,
|
|
222
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
223
|
+
});
|
|
224
|
+
return result.trim();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (!silent) {
|
|
227
|
+
throw new Error(`Git command failed: git ${args.join(' ')}\n${err.stderr || err.message}`);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================
|
|
234
|
+
// Safe Grep/Search
|
|
235
|
+
// ============================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Escape special regex characters for use in patterns.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} str - String to escape
|
|
241
|
+
* @returns {string} Escaped string safe for regex
|
|
242
|
+
*/
|
|
243
|
+
function escapeRegex(str) {
|
|
244
|
+
if (!str || typeof str !== 'string') {
|
|
245
|
+
return '';
|
|
246
|
+
}
|
|
247
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validate and sanitize a search pattern.
|
|
252
|
+
* Escapes special characters and limits length.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} pattern - Search pattern
|
|
255
|
+
* @param {Object} [options]
|
|
256
|
+
* @param {number} [options.maxLength] - Maximum pattern length
|
|
257
|
+
* @param {boolean} [options.escape] - Whether to escape regex chars
|
|
258
|
+
* @returns {string|null} Sanitized pattern or null if invalid
|
|
259
|
+
*/
|
|
260
|
+
function sanitizeSearchPattern(pattern, options = {}) {
|
|
261
|
+
const {
|
|
262
|
+
maxLength = MAX_REGEX_LENGTH,
|
|
263
|
+
escape = true
|
|
264
|
+
} = options;
|
|
265
|
+
|
|
266
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Limit length to prevent ReDoS
|
|
271
|
+
if (pattern.length > maxLength) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Optionally escape regex special characters
|
|
276
|
+
return escape ? escapeRegex(pattern) : pattern;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Execute grep safely using execFileSync.
|
|
281
|
+
* Escapes pattern to prevent regex injection.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} pattern - Search pattern (will be escaped)
|
|
284
|
+
* @param {Object} options
|
|
285
|
+
* @param {string} options.cwd - Working directory
|
|
286
|
+
* @param {string} options.searchDir - Directory to search in
|
|
287
|
+
* @param {string[]} [options.extensions] - File extensions to include
|
|
288
|
+
* @param {number} [options.maxResults] - Maximum results to return
|
|
289
|
+
* @returns {string[]} Array of matching file paths
|
|
290
|
+
*/
|
|
291
|
+
function safeGrep(pattern, options = {}) {
|
|
292
|
+
const {
|
|
293
|
+
cwd,
|
|
294
|
+
searchDir,
|
|
295
|
+
extensions = VALID_CODE_EXTENSIONS,
|
|
296
|
+
maxResults = 20
|
|
297
|
+
} = options;
|
|
298
|
+
|
|
299
|
+
// Validate and escape pattern
|
|
300
|
+
const safePattern = sanitizeSearchPattern(pattern);
|
|
301
|
+
if (!safePattern) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Build include arguments
|
|
306
|
+
const includeArgs = extensions.flatMap(ext => ['--include', `*${ext}`]);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const args = [
|
|
310
|
+
'-ril', // recursive, ignore case, files only
|
|
311
|
+
safePattern, // escaped pattern
|
|
312
|
+
...includeArgs, // file extensions
|
|
313
|
+
searchDir // search directory
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const output = safeExecFile('grep', args, {
|
|
317
|
+
cwd,
|
|
318
|
+
timeout: 5000
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return output
|
|
322
|
+
.split('\n')
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.slice(0, maxResults);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
// grep returns non-zero when no matches found - this is expected
|
|
327
|
+
if (process.env.DEBUG && err.status !== 1) {
|
|
328
|
+
console.warn(`[Security] safeGrep failed: ${err.message}`);
|
|
329
|
+
}
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Execute find safely using execFileSync.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} dir - Directory to search
|
|
338
|
+
* @param {Object} options
|
|
339
|
+
* @param {string[]} [options.extensions] - File extensions to find
|
|
340
|
+
* @param {number} [options.maxResults] - Maximum results
|
|
341
|
+
* @param {string} [options.cwd] - Working directory
|
|
342
|
+
* @returns {string[]} Array of found file paths
|
|
343
|
+
*/
|
|
344
|
+
function safeFind(dir, options = {}) {
|
|
345
|
+
const {
|
|
346
|
+
extensions = VALID_CODE_EXTENSIONS,
|
|
347
|
+
maxResults = 100,
|
|
348
|
+
cwd = process.cwd()
|
|
349
|
+
} = options;
|
|
350
|
+
|
|
351
|
+
// Build name expressions for extensions
|
|
352
|
+
const nameArgs = [];
|
|
353
|
+
extensions.forEach((ext, i) => {
|
|
354
|
+
if (i > 0) nameArgs.push('-o');
|
|
355
|
+
nameArgs.push('-name', `*${ext}`);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const args = [
|
|
360
|
+
dir,
|
|
361
|
+
'-type', 'f',
|
|
362
|
+
'(', ...nameArgs, ')'
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
const output = safeExecFile('find', args, { cwd, timeout: 10000 });
|
|
366
|
+
|
|
367
|
+
return output
|
|
368
|
+
.split('\n')
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.slice(0, maxResults);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (process.env.DEBUG) {
|
|
373
|
+
console.warn(`[Security] safeFind failed: ${err.message}`);
|
|
374
|
+
}
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ============================================================
|
|
380
|
+
// GitHub/Repository Validation
|
|
381
|
+
// ============================================================
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Validate GitHub repository format (owner/repo).
|
|
385
|
+
*
|
|
386
|
+
* @param {string} repo - Repository string to validate
|
|
387
|
+
* @returns {boolean} True if valid format
|
|
388
|
+
*/
|
|
389
|
+
function validateRepoFormat(repo) {
|
|
390
|
+
if (!repo || typeof repo !== 'string') {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
return GITHUB_REPO_PATTERN.test(repo);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Sanitize a commit message for safe use in git commands.
|
|
398
|
+
* Escapes special characters that could cause issues.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} message - Commit message to sanitize
|
|
401
|
+
* @returns {string} Sanitized message
|
|
402
|
+
*/
|
|
403
|
+
function sanitizeCommitMessage(message) {
|
|
404
|
+
if (!message || typeof message !== 'string') {
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
// Only allow basic text characters, newlines, and common punctuation
|
|
408
|
+
return message
|
|
409
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars
|
|
410
|
+
.substring(0, 5000); // Limit length
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============================================================
|
|
414
|
+
// URL Validation
|
|
415
|
+
// ============================================================
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Check if an IP address is private/internal.
|
|
419
|
+
* Used for SSRF protection.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} ip - IP address to check
|
|
422
|
+
* @returns {boolean} True if private/internal IP
|
|
423
|
+
*/
|
|
424
|
+
function isPrivateIP(ip) {
|
|
425
|
+
if (!ip) return false;
|
|
426
|
+
|
|
427
|
+
// IPv4 private ranges
|
|
428
|
+
const parts = ip.split('.').map(Number);
|
|
429
|
+
if (parts.length === 4 && parts.every(p => !isNaN(p))) {
|
|
430
|
+
// Loopback
|
|
431
|
+
if (parts[0] === 127) return true;
|
|
432
|
+
// 10.0.0.0/8
|
|
433
|
+
if (parts[0] === 10) return true;
|
|
434
|
+
// 172.16.0.0/12
|
|
435
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
436
|
+
// 192.168.0.0/16
|
|
437
|
+
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
438
|
+
// Link-local
|
|
439
|
+
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Localhost
|
|
443
|
+
if (ip === 'localhost' || ip === '::1') return true;
|
|
444
|
+
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ============================================================
|
|
449
|
+
// Exports
|
|
450
|
+
// ============================================================
|
|
451
|
+
|
|
452
|
+
module.exports = {
|
|
453
|
+
// Path validation
|
|
454
|
+
validatePathWithinProject,
|
|
455
|
+
sanitizePath,
|
|
456
|
+
|
|
457
|
+
// Command execution
|
|
458
|
+
safeExecFile,
|
|
459
|
+
safeSpawn,
|
|
460
|
+
|
|
461
|
+
// Git safety
|
|
462
|
+
validateGitRef,
|
|
463
|
+
safeGitCommand,
|
|
464
|
+
sanitizeCommitMessage,
|
|
465
|
+
|
|
466
|
+
// Search safety
|
|
467
|
+
escapeRegex,
|
|
468
|
+
sanitizeSearchPattern,
|
|
469
|
+
safeGrep,
|
|
470
|
+
safeFind,
|
|
471
|
+
|
|
472
|
+
// Repository validation
|
|
473
|
+
validateRepoFormat,
|
|
474
|
+
|
|
475
|
+
// URL/Network safety
|
|
476
|
+
isPrivateIP,
|
|
477
|
+
|
|
478
|
+
// Constants
|
|
479
|
+
MAX_REGEX_LENGTH,
|
|
480
|
+
VALID_CODE_EXTENSIONS
|
|
481
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Wogi Flow - End Session Properly
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
WORKFLOW_DIR=".workflow"
|
|
8
|
+
|
|
9
|
+
# Colors
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
YELLOW='\033[1;33m'
|
|
12
|
+
CYAN='\033[0;36m'
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
NC='\033[0m'
|
|
15
|
+
|
|
16
|
+
echo -e "${CYAN}Ending Session${NC}"
|
|
17
|
+
echo "==============="
|
|
18
|
+
echo ""
|
|
19
|
+
|
|
20
|
+
# Check config for onSessionEnd requirements
|
|
21
|
+
if [ -f "$WORKFLOW_DIR/config.json" ]; then
|
|
22
|
+
echo -e "${YELLOW}Checking session-end requirements...${NC}"
|
|
23
|
+
CONFIG_FILE="$WORKFLOW_DIR/config.json" python3 << 'EOF'
|
|
24
|
+
import json, os
|
|
25
|
+
with open(os.environ['CONFIG_FILE']) as f:
|
|
26
|
+
config = json.load(f)
|
|
27
|
+
steps = config.get('mandatorySteps', {}).get('onSessionEnd', [])
|
|
28
|
+
if steps:
|
|
29
|
+
print('Required:')
|
|
30
|
+
for step in steps:
|
|
31
|
+
print(f' • {step}')
|
|
32
|
+
EOF
|
|
33
|
+
echo ""
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Check for uncommitted changes
|
|
37
|
+
uncommitted=$(git status --porcelain 2>/dev/null | wc -l)
|
|
38
|
+
if [ $uncommitted -gt 0 ]; then
|
|
39
|
+
echo -e "${YELLOW}Uncommitted changes: $uncommitted files${NC}"
|
|
40
|
+
git status --short
|
|
41
|
+
echo ""
|
|
42
|
+
|
|
43
|
+
read -p "Commit all changes? (y/N) " confirm
|
|
44
|
+
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
|
45
|
+
read -p "Commit message: " msg
|
|
46
|
+
git add -A
|
|
47
|
+
git commit -m "${msg:-checkpoint: end of session}"
|
|
48
|
+
echo -e "${GREEN}✓ Changes committed${NC}"
|
|
49
|
+
fi
|
|
50
|
+
else
|
|
51
|
+
echo -e "${GREEN}✓ No uncommitted changes${NC}"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo ""
|
|
55
|
+
|
|
56
|
+
# Update progress.md
|
|
57
|
+
if [ -f "$WORKFLOW_DIR/state/progress.md" ]; then
|
|
58
|
+
echo -e "${YELLOW}Updating progress.md...${NC}"
|
|
59
|
+
|
|
60
|
+
# Create backup
|
|
61
|
+
cp "$WORKFLOW_DIR/state/progress.md" "$WORKFLOW_DIR/state/progress.md.bak"
|
|
62
|
+
|
|
63
|
+
# Update timestamp
|
|
64
|
+
sed -i.tmp "s/## Last Updated.*/## Last Updated\n$(date +%Y-%m-%d\ %H:%M)/" "$WORKFLOW_DIR/state/progress.md" 2>/dev/null || true
|
|
65
|
+
rm -f "$WORKFLOW_DIR/state/progress.md.tmp"
|
|
66
|
+
|
|
67
|
+
echo -e "${GREEN}✓ Progress updated${NC}"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Extract skill learnings
|
|
71
|
+
if [ -f "$WORKFLOW_DIR/config.json" ]; then
|
|
72
|
+
skill_learning=$(CONFIG_FILE="$WORKFLOW_DIR/config.json" python3 -c "
|
|
73
|
+
import json, os
|
|
74
|
+
with open(os.environ['CONFIG_FILE']) as f:
|
|
75
|
+
config = json.load(f)
|
|
76
|
+
sl = config.get('skillLearning', {})
|
|
77
|
+
enabled = sl.get('enabled', False) and sl.get('autoExtract', False)
|
|
78
|
+
print('True' if enabled else 'False')
|
|
79
|
+
" 2>/dev/null || echo "False")
|
|
80
|
+
|
|
81
|
+
if [ "$skill_learning" = "True" ]; then
|
|
82
|
+
echo ""
|
|
83
|
+
echo -e "${YELLOW}Extracting skill learnings...${NC}"
|
|
84
|
+
if command -v node &> /dev/null && [ -f "scripts/flow-skill-learn.js" ]; then
|
|
85
|
+
node scripts/flow-skill-learn.js --trigger=session-end 2>/dev/null || true
|
|
86
|
+
echo -e "${GREEN}✓ Skills updated${NC}"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
echo ""
|
|
92
|
+
|
|
93
|
+
# Push if remote exists
|
|
94
|
+
if git remote get-url origin &>/dev/null; then
|
|
95
|
+
read -p "Push to remote? (y/N) " push_confirm
|
|
96
|
+
if [ "$push_confirm" = "y" ] || [ "$push_confirm" = "Y" ]; then
|
|
97
|
+
git push
|
|
98
|
+
echo -e "${GREEN}✓ Pushed to remote${NC}"
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
echo ""
|
|
103
|
+
echo -e "${GREEN}Session ended cleanly.${NC}"
|
|
104
|
+
echo ""
|
|
105
|
+
echo "Summary:"
|
|
106
|
+
./scripts/flow status 2>/dev/null || echo " (run 'flow status' for details)"
|