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,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
|
+
}
|