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.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,1029 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Declarative Workflow Engine
5
+ *
6
+ * Conditional routing and bounded loops for automation:
7
+ * - YAML-based workflow definitions
8
+ * - Conditional step execution
9
+ * - Bounded loop iterations
10
+ * - Step dependencies
11
+ *
12
+ * Usage as module:
13
+ * const { Workflow, loadWorkflow, runWorkflow } = require('./flow-workflow');
14
+ * const workflow = loadWorkflow('deploy');
15
+ * await runWorkflow(workflow, context);
16
+ *
17
+ * Usage as CLI:
18
+ * flow workflow list # List workflows
19
+ * flow workflow run <name> # Run a workflow
20
+ * flow workflow create <name> # Create workflow template
21
+ * flow workflow validate <name> # Validate workflow
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { spawn } = require('child_process');
27
+ const { getProjectRoot, colors: c } = require('./flow-utils');
28
+
29
+ const PROJECT_ROOT = getProjectRoot();
30
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
31
+ const WORKFLOWS_DIR = path.join(WORKFLOW_DIR, 'workflows');
32
+
33
+ /**
34
+ * Validate that a path is within the project root (prevent path traversal)
35
+ */
36
+ function validatePathWithinProject(targetPath, baseRoot = PROJECT_ROOT) {
37
+ const resolvedPath = path.resolve(baseRoot, targetPath);
38
+ const resolvedRoot = path.resolve(baseRoot);
39
+
40
+ if (!resolvedPath.startsWith(resolvedRoot + path.sep) && resolvedPath !== resolvedRoot) {
41
+ throw new Error(`Path traversal detected: ${targetPath} escapes project root`);
42
+ }
43
+
44
+ return resolvedPath;
45
+ }
46
+
47
+ /**
48
+ * Step types
49
+ */
50
+ const STEP_TYPES = {
51
+ COMMAND: 'command',
52
+ SCRIPT: 'script',
53
+ GATE: 'gate',
54
+ LOOP: 'loop',
55
+ PARALLEL: 'parallel',
56
+ CONDITIONAL: 'conditional'
57
+ };
58
+
59
+ /**
60
+ * Detect project type (language, package manager)
61
+ * Returns { language, packageManager } with defaults to Node.js/npm
62
+ */
63
+ function detectProjectType(projectRoot = PROJECT_ROOT) {
64
+ // Validate projectRoot to prevent path traversal
65
+ const safeRoot = projectRoot === PROJECT_ROOT
66
+ ? PROJECT_ROOT
67
+ : validatePathWithinProject(projectRoot, PROJECT_ROOT);
68
+
69
+ // Check for Go
70
+ if (fs.existsSync(path.join(safeRoot, 'go.mod'))) {
71
+ return { language: 'go', packageManager: 'go' };
72
+ }
73
+
74
+ // Check for Rust
75
+ if (fs.existsSync(path.join(safeRoot, 'Cargo.toml'))) {
76
+ return { language: 'rust', packageManager: 'cargo' };
77
+ }
78
+
79
+ // Check for Python
80
+ if (fs.existsSync(path.join(safeRoot, 'pyproject.toml')) ||
81
+ fs.existsSync(path.join(safeRoot, 'requirements.txt'))) {
82
+ const pm = fs.existsSync(path.join(safeRoot, 'poetry.lock')) ? 'poetry'
83
+ : fs.existsSync(path.join(safeRoot, 'Pipfile.lock')) ? 'pipenv'
84
+ : 'pip';
85
+ return { language: 'python', packageManager: pm };
86
+ }
87
+
88
+ // Default to Node.js - detect specific package manager
89
+ const pm = fs.existsSync(path.join(safeRoot, 'pnpm-lock.yaml')) ? 'pnpm'
90
+ : fs.existsSync(path.join(safeRoot, 'yarn.lock')) ? 'yarn'
91
+ : fs.existsSync(path.join(safeRoot, 'bun.lockb')) ? 'bun'
92
+ : 'npm';
93
+
94
+ return { language: 'node', packageManager: pm };
95
+ }
96
+
97
+ /**
98
+ * Get quality gate command for an action (lint, test, build)
99
+ * Adapts to detected package manager and language
100
+ */
101
+ function getQualityCommand(action, projectRoot = PROJECT_ROOT) {
102
+ const { language, packageManager } = detectProjectType(projectRoot);
103
+
104
+ const commands = {
105
+ node: {
106
+ npm: { lint: 'npm run lint', test: 'npm test', build: 'npm run build', fix: 'npm run fix' },
107
+ yarn: { lint: 'yarn lint', test: 'yarn test', build: 'yarn build', fix: 'yarn fix' },
108
+ pnpm: { lint: 'pnpm lint', test: 'pnpm test', build: 'pnpm build', fix: 'pnpm fix' },
109
+ bun: { lint: 'bun run lint', test: 'bun test', build: 'bun run build', fix: 'bun run fix' }
110
+ },
111
+ python: {
112
+ pip: { lint: 'ruff check .', test: 'pytest', build: 'python -m build', fix: 'ruff check . --fix' },
113
+ poetry: { lint: 'poetry run ruff check .', test: 'poetry run pytest', build: 'poetry build', fix: 'poetry run ruff check . --fix' },
114
+ pipenv: { lint: 'pipenv run ruff check .', test: 'pipenv run pytest', build: 'pipenv run python -m build', fix: 'pipenv run ruff check . --fix' }
115
+ },
116
+ go: {
117
+ go: { lint: 'golangci-lint run', test: 'go test ./...', build: 'go build ./...', fix: 'gofmt -w .' }
118
+ },
119
+ rust: {
120
+ cargo: { lint: 'cargo clippy', test: 'cargo test', build: 'cargo build', fix: 'cargo fix --allow-dirty' }
121
+ }
122
+ };
123
+
124
+ const langCommands = commands[language] || commands.node;
125
+ const pmCommands = langCommands[packageManager] || langCommands.npm || Object.values(langCommands)[0];
126
+
127
+ return pmCommands[action] || pmCommands.lint;
128
+ }
129
+
130
+ /**
131
+ * Simple YAML parser for workflow files
132
+ */
133
+ function parseYaml(content) {
134
+ const lines = content.split('\n');
135
+ const result = {};
136
+ const stack = [{ obj: result, indent: -1 }];
137
+ let currentArray = null;
138
+
139
+ for (let i = 0; i < lines.length; i++) {
140
+ const line = lines[i];
141
+ const trimmed = line.trim();
142
+
143
+ if (!trimmed || trimmed.startsWith('#')) continue;
144
+
145
+ const indent = line.search(/\S/);
146
+
147
+ // Pop stack for lower indents
148
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
149
+ stack.pop();
150
+ }
151
+
152
+ const parent = stack[stack.length - 1].obj;
153
+
154
+ // Array item
155
+ if (trimmed.startsWith('- ')) {
156
+ const value = trimmed.slice(2).trim();
157
+
158
+ if (currentArray && Array.isArray(parent[currentArray])) {
159
+ if (value.includes(':')) {
160
+ // Object in array
161
+ const [key, ...valueParts] = value.split(':');
162
+ const obj = { [key]: valueParts.join(':').trim() };
163
+ parent[currentArray].push(obj);
164
+ stack.push({ obj: obj, indent: indent, key: currentArray });
165
+ } else {
166
+ parent[currentArray].push(value);
167
+ }
168
+ }
169
+ continue;
170
+ }
171
+
172
+ // Key-value
173
+ if (trimmed.includes(':')) {
174
+ const colonIdx = trimmed.indexOf(':');
175
+ const key = trimmed.slice(0, colonIdx).trim();
176
+ const value = trimmed.slice(colonIdx + 1).trim();
177
+
178
+ if (!value) {
179
+ // Check if next line starts with - (array)
180
+ const nextLine = lines[i + 1];
181
+ const nextTrimmed = nextLine?.trim();
182
+
183
+ if (nextTrimmed?.startsWith('- ')) {
184
+ parent[key] = [];
185
+ currentArray = key;
186
+ } else {
187
+ parent[key] = {};
188
+ stack.push({ obj: parent[key], indent: indent, key: key });
189
+ }
190
+ } else {
191
+ // Simple value
192
+ parent[key] = value === 'true' ? true :
193
+ value === 'false' ? false :
194
+ /^\d+$/.test(value) ? parseInt(value) :
195
+ value;
196
+ currentArray = null;
197
+ }
198
+ }
199
+ }
200
+
201
+ return result;
202
+ }
203
+
204
+ /**
205
+ * Generate YAML from object
206
+ */
207
+ function toYaml(obj, indent = 0) {
208
+ let result = '';
209
+ const spaces = ' '.repeat(indent);
210
+
211
+ for (const [key, value] of Object.entries(obj)) {
212
+ if (value === null || value === undefined) continue;
213
+
214
+ if (Array.isArray(value)) {
215
+ result += `${spaces}${key}:\n`;
216
+ for (const item of value) {
217
+ if (typeof item === 'object') {
218
+ result += `${spaces} - ${Object.entries(item)[0][0]}: ${Object.entries(item)[0][1]}\n`;
219
+ for (const [k, v] of Object.entries(item).slice(1)) {
220
+ result += `${spaces} ${k}: ${v}\n`;
221
+ }
222
+ } else {
223
+ result += `${spaces} - ${item}\n`;
224
+ }
225
+ }
226
+ } else if (typeof value === 'object') {
227
+ result += `${spaces}${key}:\n`;
228
+ result += toYaml(value, indent + 1);
229
+ } else {
230
+ result += `${spaces}${key}: ${value}\n`;
231
+ }
232
+ }
233
+
234
+ return result;
235
+ }
236
+
237
+ /**
238
+ * Dangerous command patterns that indicate injection attempts
239
+ * SECURITY: These patterns are blocked to prevent command injection
240
+ */
241
+ const DANGEROUS_PATTERNS = [
242
+ /;\s*rm\s+-rf/i, // ; rm -rf
243
+ /;\s*dd\s+if=/i, // ; dd if=
244
+ /;\s*mkfs/i, // ; mkfs
245
+ /;\s*wget.*\|.*sh/i, // wget | sh
246
+ /;\s*curl.*\|.*sh/i, // curl | sh
247
+ /`[^`]*`/, // Backtick command substitution
248
+ /\$\([^)]*\)/, // $() command substitution
249
+ />\s*\/dev\/(sd|hd|nvme)/i, // Write to disk devices
250
+ /;\s*:(){ :|:& };:/, // Fork bomb
251
+ /\|\s*base64\s+-d\s*\|/i, // Encoded payload execution
252
+ ];
253
+
254
+ /**
255
+ * Warning patterns - potentially dangerous but may be legitimate
256
+ */
257
+ const WARNING_PATTERNS = [
258
+ /;\s*sudo/i, // sudo in chained command
259
+ /\|\s*sh\s*$/i, // Pipe to shell
260
+ /\|\s*bash\s*$/i, // Pipe to bash
261
+ /eval\s+/i, // eval usage
262
+ ];
263
+
264
+ /**
265
+ * Validate command for injection patterns
266
+ * @param {string} command - Command to validate
267
+ * @returns {{ safe: boolean, blocked: boolean, reason?: string }}
268
+ */
269
+ function validateCommand(command) {
270
+ // Check for dangerous patterns
271
+ for (const pattern of DANGEROUS_PATTERNS) {
272
+ if (pattern.test(command)) {
273
+ return {
274
+ safe: false,
275
+ blocked: true,
276
+ reason: `Dangerous command pattern detected: ${pattern.toString()}`
277
+ };
278
+ }
279
+ }
280
+
281
+ // Check for warning patterns
282
+ for (const pattern of WARNING_PATTERNS) {
283
+ if (pattern.test(command)) {
284
+ return {
285
+ safe: false,
286
+ blocked: false,
287
+ reason: `Potentially dangerous pattern: ${pattern.toString()}`
288
+ };
289
+ }
290
+ }
291
+
292
+ return { safe: true, blocked: false };
293
+ }
294
+
295
+ /**
296
+ * Execute a shell command with security validation
297
+ *
298
+ * SECURITY: Commands are validated for injection patterns before execution.
299
+ * Dangerous patterns are blocked, warning patterns are logged.
300
+ */
301
+ function executeCommand(command, options = {}) {
302
+ return new Promise((resolve, reject) => {
303
+ // Security validation
304
+ const validation = validateCommand(command);
305
+
306
+ if (validation.blocked) {
307
+ reject(new Error(`SECURITY: Command blocked - ${validation.reason}`));
308
+ return;
309
+ }
310
+
311
+ if (!validation.safe && process.env.DEBUG) {
312
+ console.warn(`${c.yellow}Warning: ${validation.reason}${c.reset}`);
313
+ }
314
+
315
+ const startTime = Date.now();
316
+
317
+ const proc = spawn('sh', ['-c', command], {
318
+ cwd: options.cwd || PROJECT_ROOT,
319
+ env: { ...process.env, ...options.env },
320
+ timeout: options.timeout || 60000
321
+ });
322
+
323
+ let stdout = '';
324
+ let stderr = '';
325
+
326
+ proc.stdout.on('data', (data) => {
327
+ stdout += data.toString();
328
+ if (options.stream) process.stdout.write(data);
329
+ });
330
+
331
+ proc.stderr.on('data', (data) => {
332
+ stderr += data.toString();
333
+ if (options.stream) process.stderr.write(data);
334
+ });
335
+
336
+ proc.on('close', (code) => {
337
+ resolve({
338
+ exitCode: code,
339
+ stdout,
340
+ stderr,
341
+ duration: Date.now() - startTime
342
+ });
343
+ });
344
+
345
+ proc.on('error', (err) => {
346
+ reject(err);
347
+ });
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Safe condition evaluator - NO arbitrary code execution
353
+ * Only supports: ==, !=, >, <, >=, <=, &&, ||, !, true, false, strings, numbers
354
+ * Variables: $var or ${var}
355
+ *
356
+ * SECURITY: Uses whitelist parsing instead of eval/Function to prevent code injection
357
+ */
358
+ function evaluateCondition(condition, context) {
359
+ // Replace variables first
360
+ let expr = condition.replace(/\$\{?(\w+)\}?/g, (match, name) => {
361
+ const value = context[name];
362
+ if (typeof value === 'string') return JSON.stringify(value);
363
+ if (typeof value === 'boolean') return value.toString();
364
+ if (typeof value === 'number') return value.toString();
365
+ return 'undefined';
366
+ });
367
+
368
+ // Use safe parser instead of eval
369
+ return safeEvaluate(expr);
370
+ }
371
+
372
+ /**
373
+ * Parse a literal value safely - only allows primitives
374
+ */
375
+ function parseValue(str) {
376
+ str = str.trim();
377
+
378
+ // Boolean
379
+ if (str === 'true') return true;
380
+ if (str === 'false') return false;
381
+ if (str === 'undefined' || str === 'null') return undefined;
382
+
383
+ // Number
384
+ if (/^-?\d+(\.\d+)?$/.test(str)) return parseFloat(str);
385
+
386
+ // String (quoted)
387
+ if ((str.startsWith('"') && str.endsWith('"')) ||
388
+ (str.startsWith("'") && str.endsWith("'"))) {
389
+ return str.slice(1, -1);
390
+ }
391
+
392
+ // Unquoted string (identifier-like)
393
+ return str;
394
+ }
395
+
396
+ /**
397
+ * Safe expression evaluator - whitelist approach only
398
+ * Only allows: comparisons, logical operators, literals
399
+ *
400
+ * SECURITY: No eval, no Function, no dynamic code execution
401
+ */
402
+ function safeEvaluate(expr) {
403
+ expr = expr.trim();
404
+
405
+ // Handle parentheses recursively
406
+ while (expr.includes('(')) {
407
+ expr = expr.replace(/\(([^()]+)\)/g, (_, inner) => {
408
+ return safeEvaluate(inner) ? 'true' : 'false';
409
+ });
410
+ }
411
+
412
+ // Handle logical OR (lowest precedence)
413
+ if (expr.includes('||')) {
414
+ const parts = splitOnOperator(expr, '||');
415
+ return parts.some(part => safeEvaluate(part.trim()));
416
+ }
417
+
418
+ // Handle logical AND
419
+ if (expr.includes('&&')) {
420
+ const parts = splitOnOperator(expr, '&&');
421
+ return parts.every(part => safeEvaluate(part.trim()));
422
+ }
423
+
424
+ // Handle NOT
425
+ if (expr.startsWith('!')) {
426
+ return !safeEvaluate(expr.slice(1).trim());
427
+ }
428
+
429
+ // Handle comparisons (order matters - check longer operators first)
430
+ const compMatch = expr.match(/^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/);
431
+ if (compMatch) {
432
+ const left = parseValue(compMatch[1].trim());
433
+ const op = compMatch[2];
434
+ const right = parseValue(compMatch[3].trim());
435
+
436
+ switch (op) {
437
+ case '==':
438
+ case '===':
439
+ return left === right;
440
+ case '!=':
441
+ case '!==':
442
+ return left !== right;
443
+ case '>':
444
+ return left > right;
445
+ case '>=':
446
+ return left >= right;
447
+ case '<':
448
+ return left < right;
449
+ case '<=':
450
+ return left <= right;
451
+ default:
452
+ return false;
453
+ }
454
+ }
455
+
456
+ // Handle simple boolean/truthy value
457
+ const val = parseValue(expr);
458
+ return val === true || val === 'true' || (typeof val === 'number' && val !== 0);
459
+ }
460
+
461
+ /**
462
+ * Split expression on operator, respecting quoted strings
463
+ */
464
+ function splitOnOperator(expr, op) {
465
+ const parts = [];
466
+ let current = '';
467
+ let inQuote = false;
468
+ let quoteChar = '';
469
+
470
+ for (let i = 0; i < expr.length; i++) {
471
+ const char = expr[i];
472
+
473
+ if ((char === '"' || char === "'") && (i === 0 || expr[i-1] !== '\\')) {
474
+ if (!inQuote) {
475
+ inQuote = true;
476
+ quoteChar = char;
477
+ } else if (char === quoteChar) {
478
+ inQuote = false;
479
+ }
480
+ }
481
+
482
+ if (!inQuote && expr.slice(i, i + op.length) === op) {
483
+ parts.push(current);
484
+ current = '';
485
+ i += op.length - 1;
486
+ } else {
487
+ current += char;
488
+ }
489
+ }
490
+
491
+ parts.push(current);
492
+ return parts;
493
+ }
494
+
495
+ /**
496
+ * Workflow execution context
497
+ */
498
+ class WorkflowContext {
499
+ constructor(initialVars = {}) {
500
+ this.variables = { ...initialVars };
501
+ this.stepResults = {};
502
+ this.iteration = 0;
503
+ this.maxIterations = 100;
504
+ }
505
+
506
+ get(key) {
507
+ return this.variables[key];
508
+ }
509
+
510
+ set(key, value) {
511
+ this.variables[key] = value;
512
+ }
513
+
514
+ setResult(stepId, result) {
515
+ this.stepResults[stepId] = result;
516
+ this.variables[`${stepId}_exitCode`] = result.exitCode;
517
+ this.variables[`${stepId}_success`] = result.exitCode === 0;
518
+ }
519
+
520
+ getResult(stepId) {
521
+ return this.stepResults[stepId];
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Workflow class
527
+ */
528
+ class Workflow {
529
+ constructor(definition) {
530
+ this.name = definition.name || 'unnamed';
531
+ this.description = definition.description || '';
532
+ this.steps = definition.steps || [];
533
+ this.variables = definition.variables || {};
534
+ this.onError = definition.onError || 'abort';
535
+ this.maxIterations = definition.maxIterations || 100;
536
+ }
537
+
538
+ async run(context = null) {
539
+ context = context || new WorkflowContext(this.variables);
540
+ context.maxIterations = this.maxIterations;
541
+
542
+ const results = {
543
+ name: this.name,
544
+ success: true,
545
+ steps: [],
546
+ startTime: new Date().toISOString(),
547
+ endTime: null
548
+ };
549
+
550
+ for (const step of this.steps) {
551
+ try {
552
+ const stepResult = await this.runStep(step, context);
553
+ results.steps.push(stepResult);
554
+
555
+ if (!stepResult.success && this.onError === 'abort') {
556
+ results.success = false;
557
+ break;
558
+ }
559
+ } catch (err) {
560
+ results.steps.push({
561
+ id: step.id || step.name,
562
+ success: false,
563
+ error: err.message
564
+ });
565
+ results.success = false;
566
+
567
+ if (this.onError === 'abort') break;
568
+ }
569
+ }
570
+
571
+ results.endTime = new Date().toISOString();
572
+ return results;
573
+ }
574
+
575
+ async runStep(step, context) {
576
+ const stepId = step.id || step.name;
577
+ const stepResult = {
578
+ id: stepId,
579
+ type: step.type || STEP_TYPES.COMMAND,
580
+ success: true,
581
+ skipped: false,
582
+ duration: 0
583
+ };
584
+
585
+ // Check condition
586
+ if (step.when) {
587
+ const shouldRun = evaluateCondition(step.when, context.variables);
588
+ if (!shouldRun) {
589
+ stepResult.skipped = true;
590
+ stepResult.skipReason = 'Condition not met';
591
+ return stepResult;
592
+ }
593
+ }
594
+
595
+ const startTime = Date.now();
596
+
597
+ switch (step.type) {
598
+ case STEP_TYPES.COMMAND:
599
+ case undefined: {
600
+ const result = await executeCommand(step.run || step.command, {
601
+ timeout: step.timeout,
602
+ stream: step.stream
603
+ });
604
+ stepResult.exitCode = result.exitCode;
605
+ stepResult.success = result.exitCode === 0;
606
+ stepResult.stdout = result.stdout;
607
+ stepResult.stderr = result.stderr;
608
+ context.setResult(stepId, result);
609
+ break;
610
+ }
611
+
612
+ case STEP_TYPES.GATE: {
613
+ const result = await executeCommand(step.check, { timeout: step.timeout });
614
+ stepResult.success = result.exitCode === 0;
615
+ stepResult.exitCode = result.exitCode;
616
+
617
+ if (!stepResult.success && step.onFail) {
618
+ console.log(`${c.yellow}Gate failed, running recovery...${c.reset}`);
619
+ await executeCommand(step.onFail);
620
+ }
621
+ break;
622
+ }
623
+
624
+ case STEP_TYPES.LOOP: {
625
+ const maxIter = step.maxIterations || context.maxIterations;
626
+ let iterations = 0;
627
+
628
+ while (iterations < maxIter) {
629
+ context.iteration = iterations;
630
+
631
+ // Check exit condition
632
+ if (step.until && evaluateCondition(step.until, context.variables)) {
633
+ break;
634
+ }
635
+
636
+ // Run loop body
637
+ for (const innerStep of step.steps || []) {
638
+ await this.runStep(innerStep, context);
639
+ }
640
+
641
+ iterations++;
642
+
643
+ // Check continue condition
644
+ if (step.while && !evaluateCondition(step.while, context.variables)) {
645
+ break;
646
+ }
647
+ }
648
+
649
+ stepResult.iterations = iterations;
650
+ stepResult.success = iterations < maxIter || step.allowMaxIterations;
651
+ break;
652
+ }
653
+
654
+ case STEP_TYPES.PARALLEL: {
655
+ const promises = (step.steps || []).map(s => this.runStep(s, context));
656
+ const results = await Promise.all(promises);
657
+ stepResult.parallelResults = results;
658
+ stepResult.success = results.every(r => r.success);
659
+ break;
660
+ }
661
+
662
+ case STEP_TYPES.CONDITIONAL: {
663
+ const branches = step.branches || [];
664
+ let executed = false;
665
+
666
+ for (const branch of branches) {
667
+ if (evaluateCondition(branch.when, context.variables)) {
668
+ for (const innerStep of branch.steps || []) {
669
+ await this.runStep(innerStep, context);
670
+ }
671
+ executed = true;
672
+ break;
673
+ }
674
+ }
675
+
676
+ if (!executed && step.else) {
677
+ for (const innerStep of step.else || []) {
678
+ await this.runStep(innerStep, context);
679
+ }
680
+ }
681
+
682
+ stepResult.success = true;
683
+ break;
684
+ }
685
+
686
+ default:
687
+ stepResult.error = `Unknown step type: ${step.type}`;
688
+ stepResult.success = false;
689
+ }
690
+
691
+ stepResult.duration = Date.now() - startTime;
692
+ return stepResult;
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Load workflow from file
698
+ */
699
+ function loadWorkflow(name) {
700
+ const yamlPath = path.join(WORKFLOWS_DIR, `${name}.yaml`);
701
+ const ymlPath = path.join(WORKFLOWS_DIR, `${name}.yml`);
702
+ const jsonPath = path.join(WORKFLOWS_DIR, `${name}.json`);
703
+
704
+ let definition = null;
705
+
706
+ if (fs.existsSync(yamlPath)) {
707
+ definition = parseYaml(fs.readFileSync(yamlPath, 'utf-8'));
708
+ } else if (fs.existsSync(ymlPath)) {
709
+ definition = parseYaml(fs.readFileSync(ymlPath, 'utf-8'));
710
+ } else if (fs.existsSync(jsonPath)) {
711
+ definition = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
712
+ } else {
713
+ throw new Error(`Workflow not found: ${name}`);
714
+ }
715
+
716
+ return new Workflow(definition);
717
+ }
718
+
719
+ /**
720
+ * List available workflows
721
+ */
722
+ function listWorkflows() {
723
+ if (!fs.existsSync(WORKFLOWS_DIR)) {
724
+ return [];
725
+ }
726
+
727
+ const files = fs.readdirSync(WORKFLOWS_DIR);
728
+ const workflows = [];
729
+
730
+ for (const file of files) {
731
+ if (file.endsWith('.yaml') || file.endsWith('.yml') || file.endsWith('.json')) {
732
+ const name = file.replace(/\.(yaml|yml|json)$/, '');
733
+ try {
734
+ const workflow = loadWorkflow(name);
735
+ workflows.push({
736
+ name,
737
+ description: workflow.description,
738
+ steps: workflow.steps.length
739
+ });
740
+ } catch {
741
+ workflows.push({ name, error: 'Failed to parse' });
742
+ }
743
+ }
744
+ }
745
+
746
+ return workflows;
747
+ }
748
+
749
+ /**
750
+ * Create workflow template
751
+ */
752
+ function createWorkflowTemplate(name) {
753
+ if (!fs.existsSync(WORKFLOWS_DIR)) {
754
+ fs.mkdirSync(WORKFLOWS_DIR, { recursive: true });
755
+ }
756
+
757
+ // Get language-appropriate commands
758
+ const lintCmd = getQualityCommand('lint');
759
+ const testCmd = getQualityCommand('test');
760
+ const buildCmd = getQualityCommand('build');
761
+ const fixCmd = getQualityCommand('fix');
762
+
763
+ const template = {
764
+ name,
765
+ description: 'Workflow description',
766
+ variables: {
767
+ environment: 'development'
768
+ },
769
+ onError: 'abort',
770
+ maxIterations: 10,
771
+ steps: [
772
+ {
773
+ id: 'lint',
774
+ name: 'Run linting',
775
+ run: lintCmd
776
+ },
777
+ {
778
+ id: 'test',
779
+ name: 'Run tests',
780
+ run: testCmd,
781
+ when: '$environment == "development"'
782
+ },
783
+ {
784
+ id: 'build',
785
+ name: 'Build project',
786
+ run: buildCmd
787
+ },
788
+ {
789
+ id: 'retry-loop',
790
+ name: 'Retry on failure',
791
+ type: 'loop',
792
+ maxIterations: 3,
793
+ until: '$build_success == true',
794
+ steps: [
795
+ {
796
+ id: 'fix-attempt',
797
+ run: fixCmd
798
+ }
799
+ ]
800
+ }
801
+ ]
802
+ };
803
+
804
+ const yamlContent = `# ${name} Workflow
805
+ # Auto-generated template
806
+
807
+ ${toYaml(template)}`;
808
+
809
+ const filePath = path.join(WORKFLOWS_DIR, `${name}.yaml`);
810
+ fs.writeFileSync(filePath, yamlContent);
811
+
812
+ return filePath;
813
+ }
814
+
815
+ /**
816
+ * Validate workflow
817
+ */
818
+ function validateWorkflow(name) {
819
+ const errors = [];
820
+ const warnings = [];
821
+
822
+ try {
823
+ const workflow = loadWorkflow(name);
824
+
825
+ if (!workflow.name) {
826
+ warnings.push('Missing workflow name');
827
+ }
828
+
829
+ if (!workflow.steps || workflow.steps.length === 0) {
830
+ errors.push('Workflow has no steps');
831
+ }
832
+
833
+ for (const step of workflow.steps || []) {
834
+ if (!step.id && !step.name) {
835
+ errors.push(`Step missing id/name: ${JSON.stringify(step).slice(0, 50)}`);
836
+ }
837
+
838
+ if (step.type === 'loop' && !step.until && !step.while && !step.maxIterations) {
839
+ warnings.push(`Loop step "${step.id || step.name}" has no exit condition`);
840
+ }
841
+ }
842
+ } catch (err) {
843
+ errors.push(`Parse error: ${err.message}`);
844
+ }
845
+
846
+ return { valid: errors.length === 0, errors, warnings };
847
+ }
848
+
849
+ // Module exports
850
+ module.exports = {
851
+ STEP_TYPES,
852
+ Workflow,
853
+ WorkflowContext,
854
+ loadWorkflow,
855
+ listWorkflows,
856
+ createWorkflowTemplate,
857
+ validateWorkflow,
858
+ executeCommand,
859
+ evaluateCondition,
860
+ // Security utilities
861
+ validateCommand,
862
+ DANGEROUS_PATTERNS,
863
+ WARNING_PATTERNS,
864
+ // Language-agnostic quality commands
865
+ detectProjectType,
866
+ getQualityCommand
867
+ };
868
+
869
+ // CLI Handler
870
+ if (require.main === module) {
871
+ const args = process.argv.slice(2);
872
+ const command = args[0];
873
+
874
+ async function main() {
875
+ switch (command) {
876
+ case 'list': {
877
+ const workflows = listWorkflows();
878
+
879
+ if (workflows.length === 0) {
880
+ console.log(`${c.dim}No workflows found.${c.reset}`);
881
+ console.log(`${c.dim}Create one with: flow workflow create <name>${c.reset}`);
882
+ return;
883
+ }
884
+
885
+ console.log(`\n${c.cyan}${c.bold}Available Workflows${c.reset}\n`);
886
+
887
+ for (const wf of workflows) {
888
+ if (wf.error) {
889
+ console.log(`${c.red}✗${c.reset} ${wf.name} ${c.dim}(${wf.error})${c.reset}`);
890
+ } else {
891
+ console.log(`${c.green}✓${c.reset} ${c.bold}${wf.name}${c.reset}`);
892
+ if (wf.description) {
893
+ console.log(` ${c.dim}${wf.description}${c.reset}`);
894
+ }
895
+ console.log(` ${c.dim}${wf.steps} step(s)${c.reset}`);
896
+ }
897
+ }
898
+ break;
899
+ }
900
+
901
+ case 'run': {
902
+ const name = args[1];
903
+ if (!name) {
904
+ console.error(`${c.red}Error: Workflow name required${c.reset}`);
905
+ process.exit(1);
906
+ }
907
+
908
+ console.log(`${c.cyan}Running workflow: ${name}${c.reset}\n`);
909
+
910
+ try {
911
+ const workflow = loadWorkflow(name);
912
+ const results = await workflow.run();
913
+
914
+ console.log('');
915
+ for (const step of results.steps) {
916
+ const icon = step.skipped ? `${c.dim}○` :
917
+ step.success ? `${c.green}✓` : `${c.red}✗`;
918
+ const status = step.skipped ? 'skipped' :
919
+ step.success ? 'passed' : 'failed';
920
+ console.log(`${icon}${c.reset} ${step.id} ${c.dim}(${status}, ${step.duration}ms)${c.reset}`);
921
+ }
922
+
923
+ console.log('');
924
+ if (results.success) {
925
+ console.log(`${c.green}✅ Workflow completed successfully${c.reset}`);
926
+ } else {
927
+ console.log(`${c.red}❌ Workflow failed${c.reset}`);
928
+ process.exit(1);
929
+ }
930
+ } catch (err) {
931
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
932
+ process.exit(1);
933
+ }
934
+ break;
935
+ }
936
+
937
+ case 'create': {
938
+ const name = args[1];
939
+ if (!name) {
940
+ console.error(`${c.red}Error: Workflow name required${c.reset}`);
941
+ process.exit(1);
942
+ }
943
+
944
+ const filePath = createWorkflowTemplate(name);
945
+ console.log(`${c.green}✅ Created workflow: ${filePath}${c.reset}`);
946
+ break;
947
+ }
948
+
949
+ case 'validate': {
950
+ const name = args[1];
951
+ if (!name) {
952
+ console.error(`${c.red}Error: Workflow name required${c.reset}`);
953
+ process.exit(1);
954
+ }
955
+
956
+ const result = validateWorkflow(name);
957
+
958
+ if (result.valid) {
959
+ console.log(`${c.green}✅ Workflow "${name}" is valid${c.reset}`);
960
+ } else {
961
+ console.log(`${c.red}❌ Workflow "${name}" has errors:${c.reset}`);
962
+ for (const err of result.errors) {
963
+ console.log(` ${c.red}• ${err}${c.reset}`);
964
+ }
965
+ }
966
+
967
+ if (result.warnings.length > 0) {
968
+ console.log(`\n${c.yellow}Warnings:${c.reset}`);
969
+ for (const warn of result.warnings) {
970
+ console.log(` ${c.yellow}• ${warn}${c.reset}`);
971
+ }
972
+ }
973
+
974
+ process.exit(result.valid ? 0 : 1);
975
+ }
976
+
977
+ default: {
978
+ console.log(`
979
+ ${c.cyan}Wogi Flow - Declarative Workflow Engine${c.reset}
980
+
981
+ ${c.bold}Usage:${c.reset}
982
+ flow workflow list List available workflows
983
+ flow workflow run <name> Run a workflow
984
+ flow workflow create <name> Create workflow template
985
+ flow workflow validate <name> Validate workflow syntax
986
+
987
+ ${c.bold}Workflow YAML Format:${c.reset}
988
+ name: my-workflow
989
+ description: Description here
990
+ onError: abort # abort | continue
991
+ maxIterations: 10
992
+
993
+ steps:
994
+ - id: lint
995
+ run: <lint-command> # Auto-detected: npm/yarn/pnpm/cargo/go/ruff
996
+
997
+ - id: conditional-test
998
+ when: \$environment == "dev"
999
+ run: <test-command> # Auto-detected based on project type
1000
+
1001
+ - id: retry-loop
1002
+ type: loop
1003
+ maxIterations: 3
1004
+ until: \$build_success == true
1005
+ steps:
1006
+ - run: <build-command>
1007
+
1008
+ ${c.bold}Language Support:${c.reset}
1009
+ Node.js npm/yarn/pnpm/bun (auto-detected from lock file)
1010
+ Python pip/poetry/pipenv (pytest, ruff)
1011
+ Go go test, golangci-lint
1012
+ Rust cargo test, cargo clippy
1013
+
1014
+ ${c.bold}Step Types:${c.reset}
1015
+ command Run shell command (default)
1016
+ gate Verification gate with recovery
1017
+ loop Bounded iteration
1018
+ parallel Run steps in parallel
1019
+ conditional Branch based on conditions
1020
+ `);
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ main().catch(err => {
1026
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
1027
+ process.exit(1);
1028
+ });
1029
+ }