swarm-orchestrator 7.0.0-alpha.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/LICENSE +15 -0
- package/README.md +199 -0
- package/agents/reviewer.agent.md +50 -0
- package/agents/worker.agent.md +41 -0
- package/config/default-agents.yaml +56 -0
- package/config/pm-agent.yaml +36 -0
- package/config/quality-gates.yaml +88 -0
- package/config/repair-agent.yaml +34 -0
- package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
- package/config/semgrep-rules/exception-swallowing.yaml +13 -0
- package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
- package/config/semgrep-rules/mock-mutation.yaml +6 -0
- package/config/semgrep-rules/test-modification.yaml +6 -0
- package/config/user-agents.yaml +23 -0
- package/dist/src/adapters/adapter-factory.js +29 -0
- package/dist/src/adapters/agent-adapter.js +32 -0
- package/dist/src/adapters/claude-code-adapter.js +126 -0
- package/dist/src/adapters/claude-code-teams.js +119 -0
- package/dist/src/adapters/codex-adapter.js +118 -0
- package/dist/src/adapters/copilot-adapter.js +224 -0
- package/dist/src/adapters/fatal-error-classifier.js +88 -0
- package/dist/src/adapters/index.js +44 -0
- package/dist/src/adapters/persistent-session.js +204 -0
- package/dist/src/adapters/process-supervisor.js +199 -0
- package/dist/src/agents-exporter.js +398 -0
- package/dist/src/analytics-log.js +161 -0
- package/dist/src/baseline-scanner.js +65 -0
- package/dist/src/bootstrap-evidence.js +88 -0
- package/dist/src/bootstrap-orchestrator.js +205 -0
- package/dist/src/bootstrap-types.js +5 -0
- package/dist/src/branch-merger.js +427 -0
- package/dist/src/cli/attest-handlers.js +44 -0
- package/dist/src/cli/cost-prompt.js +51 -0
- package/dist/src/cli/demo-handlers.js +274 -0
- package/dist/src/cli/flags.js +152 -0
- package/dist/src/cli/index.js +57 -0
- package/dist/src/cli/live-status.js +186 -0
- package/dist/src/cli/misc-handlers.js +242 -0
- package/dist/src/cli/plan-handlers.js +350 -0
- package/dist/src/cli/share-handlers.js +163 -0
- package/dist/src/cli/status-handlers.js +457 -0
- package/dist/src/cli/swarm-handlers.js +638 -0
- package/dist/src/cli/usage.js +72 -0
- package/dist/src/cli.js +217 -0
- package/dist/src/commit-pattern-detector.js +143 -0
- package/dist/src/commit-quality-analyzer.js +38 -0
- package/dist/src/config-loader.js +339 -0
- package/dist/src/context-broker.js +358 -0
- package/dist/src/copilot-cli-wrapper.js +258 -0
- package/dist/src/cost-estimator.js +241 -0
- package/dist/src/defaults.js +18 -0
- package/dist/src/demo-mode.js +180 -0
- package/dist/src/deployment-handler.js +78 -0
- package/dist/src/deployment-manager.js +228 -0
- package/dist/src/execution-queue.js +196 -0
- package/dist/src/external-tool-manager.js +184 -0
- package/dist/src/gate-prompt-builder.js +117 -0
- package/dist/src/gate-remediation.js +173 -0
- package/dist/src/github-issues-ingester.js +152 -0
- package/dist/src/github-mcp-integrator.js +164 -0
- package/dist/src/hook-generator.js +229 -0
- package/dist/src/index.js +48 -0
- package/dist/src/knowledge-base.js +300 -0
- package/dist/src/logger.js +111 -0
- package/dist/src/meta-analyzer.js +208 -0
- package/dist/src/metrics-collector.js +209 -0
- package/dist/src/metrics-types.js +5 -0
- package/dist/src/multi-repo-coordinator.js +179 -0
- package/dist/src/orchestrator/async-meta-analysis.js +84 -0
- package/dist/src/orchestrator/fatal-run-error.js +43 -0
- package/dist/src/orchestrator/final-gates-remediation.js +258 -0
- package/dist/src/orchestrator/git-state-utils.js +119 -0
- package/dist/src/orchestrator/pause-controller.js +55 -0
- package/dist/src/orchestrator/replan-runner.js +258 -0
- package/dist/src/orchestrator/step-executor.js +391 -0
- package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
- package/dist/src/owasp-mapper.js +153 -0
- package/dist/src/owasp-report-renderer.js +39 -0
- package/dist/src/plan-files.js +142 -0
- package/dist/src/plan-generator.js +1207 -0
- package/dist/src/pm-agent.js +296 -0
- package/dist/src/post-run-reporter.js +203 -0
- package/dist/src/pr-automation.js +103 -0
- package/dist/src/pr-manager.js +318 -0
- package/dist/src/presenter/index.js +209 -0
- package/dist/src/prompt-builder.js +173 -0
- package/dist/src/quality-gates/config-loader.js +196 -0
- package/dist/src/quality-gates/default-config.js +243 -0
- package/dist/src/quality-gates/file-utils.js +152 -0
- package/dist/src/quality-gates/gate-runner.js +171 -0
- package/dist/src/quality-gates/gates/accessibility.js +355 -0
- package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
- package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
- package/dist/src/quality-gates/gates/readme-claims.js +62 -0
- package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
- package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
- package/dist/src/quality-gates/gates/test-coverage.js +240 -0
- package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
- package/dist/src/quality-gates/gates/test-isolation.js +74 -0
- package/dist/src/quality-gates/index.js +17 -0
- package/dist/src/quality-gates/registry.js +205 -0
- package/dist/src/quality-gates/types.js +2 -0
- package/dist/src/quick-fix-mode.js +302 -0
- package/dist/src/recipe-loader.js +122 -0
- package/dist/src/repair-agent.js +396 -0
- package/dist/src/repo-analyzer.js +342 -0
- package/dist/src/report-generator.js +146 -0
- package/dist/src/report-renderer.js +90 -0
- package/dist/src/requirement-filter.js +52 -0
- package/dist/src/sarif-formatter.js +140 -0
- package/dist/src/scheduling/dependency-analyzer.js +178 -0
- package/dist/src/scheduling/work-stealing-queue.js +123 -0
- package/dist/src/secret-redactor.js +90 -0
- package/dist/src/session-executor.js +587 -0
- package/dist/src/session-manager.js +221 -0
- package/dist/src/share/transcript-verification.js +128 -0
- package/dist/src/share-parser.js +477 -0
- package/dist/src/spinner.js +146 -0
- package/dist/src/step-runner.js +365 -0
- package/dist/src/swarm-orchestrator.js +686 -0
- package/dist/src/task-classifier.js +134 -0
- package/dist/src/test-command-discovery.js +123 -0
- package/dist/src/text-similarity.js +24 -0
- package/dist/src/tier-maps.js +158 -0
- package/dist/src/types.js +5 -0
- package/dist/src/url-shortener.js +135 -0
- package/dist/src/verification/attestation.js +166 -0
- package/dist/src/verification/cheat-detector.js +240 -0
- package/dist/src/verification/command-runner.js +70 -0
- package/dist/src/verification/composite-score.js +157 -0
- package/dist/src/verification/cosign-attestation.js +137 -0
- package/dist/src/verification/diff-analysis.js +89 -0
- package/dist/src/verification/differential-gate.js +173 -0
- package/dist/src/verification/index.js +41 -0
- package/dist/src/verification/mutation-gate.js +242 -0
- package/dist/src/verification/property-gate.js +201 -0
- package/dist/src/verification/test-synthesizer.js +243 -0
- package/dist/src/verifier/outcome-checks.js +396 -0
- package/dist/src/verifier/transcript-checks.js +207 -0
- package/dist/src/verifier/verification-reporters.js +148 -0
- package/dist/src/verifier-engine.js +491 -0
- package/dist/src/wave-resizer.js +131 -0
- package/dist/src/wave-scheduler.js +44 -0
- package/dist/src/worktree-manager.js +368 -0
- package/dist/src/worktree-reserved-paths.js +97 -0
- package/package.json +80 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/backend_master.agent.md +72 -0
- package/plugin/agents/devops_pro.agent.md +45 -0
- package/plugin/agents/frontend_expert.agent.md +59 -0
- package/plugin/agents/integrator_finalizer.agent.md +44 -0
- package/plugin/agents/meta_reviewer.agent.md +44 -0
- package/plugin/agents/security_auditor.agent.md +43 -0
- package/plugin/agents/tester_elite.agent.md +43 -0
- package/plugin/hooks/hooks.json +19 -0
- package/plugin/hooks/post-tool-use.json +9 -0
- package/plugin/hooks/pre-tool-use.json +9 -0
- package/plugin/plugin.json +47 -0
- package/plugin/skills/swarm-gates/SKILL.md +53 -0
- package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
- package/plugin/skills/swarm-verify/SKILL.md +44 -0
- package/templates/README.md +35 -0
- package/templates/cli-tool.json +31 -0
- package/templates/fullstack.json +52 -0
- package/templates/library.json +38 -0
- package/templates/react-app.json +45 -0
- package/templates/recipes/add-api-docs.json +35 -0
- package/templates/recipes/add-auth.json +47 -0
- package/templates/recipes/add-ci.json +39 -0
- package/templates/recipes/add-tests.json +39 -0
- package/templates/recipes/migrate-to-ts.json +42 -0
- package/templates/recipes/refactor-modularize.json +44 -0
- package/templates/recipes/security-audit.json +35 -0
- package/templates/rest-api.json +38 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DemoMode = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Pre-configured demo scenarios for showcasing swarm orchestrator
|
|
41
|
+
*/
|
|
42
|
+
class DemoMode {
|
|
43
|
+
scenariosDir;
|
|
44
|
+
constructor(scenariosDir) {
|
|
45
|
+
this.scenariosDir = scenariosDir || path.join(process.cwd(), 'demos');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get all available demo scenarios
|
|
49
|
+
*/
|
|
50
|
+
getAvailableScenarios() {
|
|
51
|
+
return [
|
|
52
|
+
this.getDemoFastScenario(),
|
|
53
|
+
this.getApiQuickScenario()
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Demo Fast - "hello world" swarm
|
|
58
|
+
* Two agents, one wave, minimal work.
|
|
59
|
+
* Both steps are truly independent - no shared files.
|
|
60
|
+
*/
|
|
61
|
+
getDemoFastScenario() {
|
|
62
|
+
return {
|
|
63
|
+
name: 'demo-fast',
|
|
64
|
+
description: 'Two independent utility files, one wave',
|
|
65
|
+
goal: 'Two independent micro-tasks: each agent adds one small TypeScript utility file.',
|
|
66
|
+
expectedDuration: '20-30 seconds',
|
|
67
|
+
steps: [
|
|
68
|
+
{
|
|
69
|
+
stepNumber: 1,
|
|
70
|
+
agentName: 'backend_master',
|
|
71
|
+
task: 'Create a tiny TypeScript utility module at src/string-utils.ts that exports a function greet(name: string): string which returns "Hello, <name>!". Keep it boring. No new deps. Add a short top-of-file comment. Commit your work.',
|
|
72
|
+
dependencies: [],
|
|
73
|
+
expectedOutputs: [
|
|
74
|
+
'src/string-utils.ts with exported greet() function'
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
stepNumber: 2,
|
|
79
|
+
agentName: 'frontend_expert',
|
|
80
|
+
task: 'Create a tiny TypeScript utility module at src/number-utils.ts that exports a function double(n: number): number which returns n * 2. Keep it boring. No new deps. Add a short top-of-file comment. Commit your work.',
|
|
81
|
+
dependencies: [],
|
|
82
|
+
expectedOutputs: [
|
|
83
|
+
'src/number-utils.ts with exported double() function'
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* API Quick: 3-step REST API build showing wave dependencies.
|
|
91
|
+
* Step 1: Worker builds the endpoints.
|
|
92
|
+
* Step 2: Worker adds tests (depends on step 1).
|
|
93
|
+
* Step 3: Worker adds a Dockerfile (depends on step 1).
|
|
94
|
+
*/
|
|
95
|
+
getApiQuickScenario() {
|
|
96
|
+
return {
|
|
97
|
+
name: 'api-quick',
|
|
98
|
+
description: 'REST API with tests and Dockerfile, dependency-chained across two waves',
|
|
99
|
+
goal: 'Build a minimal REST API with health and items CRUD, add tests, and containerize with Docker',
|
|
100
|
+
expectedDuration: '4-6 minutes',
|
|
101
|
+
steps: [
|
|
102
|
+
{
|
|
103
|
+
stepNumber: 1,
|
|
104
|
+
agentName: 'backend_master',
|
|
105
|
+
task: 'Create a Node.js REST API with Express. Endpoints: GET /health returning { status: "ok" }, GET /api/items returning an in-memory array, POST /api/items accepting { name } and returning the created item with a generated id. Add input validation (reject empty name). Export the app for testing. Add a start script to package.json. Commit your work.',
|
|
106
|
+
dependencies: [],
|
|
107
|
+
expectedOutputs: [
|
|
108
|
+
'server.js with /health, GET /api/items, POST /api/items',
|
|
109
|
+
'package.json with start script',
|
|
110
|
+
'Input validation for name field'
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
stepNumber: 2,
|
|
115
|
+
agentName: 'tester_elite',
|
|
116
|
+
task: 'Add tests for the REST API created in step 1. Use the Node.js built-in test runner (node:test and node:assert/strict). Test: GET /health returns 200 and { status: "ok" }, GET /api/items returns empty array initially, POST /api/items with valid name returns 201, POST /api/items with empty name returns 400, GET /api/items after POST includes the new item. Import the app from server.js and start/stop it in before/after hooks. Add a test script to package.json. Commit your work.',
|
|
117
|
+
dependencies: [1],
|
|
118
|
+
expectedOutputs: [
|
|
119
|
+
'test/api.test.js with 5+ test cases',
|
|
120
|
+
'Tests use node:test and node:assert/strict',
|
|
121
|
+
'package.json test script'
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
stepNumber: 3,
|
|
126
|
+
agentName: 'devops_pro',
|
|
127
|
+
task: 'Add a Dockerfile for the Node.js REST API. Use node:20-alpine base image, copy package.json first for layer caching, run npm ci --omit=dev, copy source files, expose port 3000, set NODE_ENV=production, and use CMD ["node", "server.js"]. Add a .dockerignore excluding node_modules, .git, and test/. Commit your work.',
|
|
128
|
+
dependencies: [1],
|
|
129
|
+
expectedOutputs: [
|
|
130
|
+
'Dockerfile with multi-layer caching',
|
|
131
|
+
'.dockerignore'
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get scenario by name
|
|
139
|
+
*/
|
|
140
|
+
getScenario(name) {
|
|
141
|
+
const scenarios = this.getAvailableScenarios();
|
|
142
|
+
return scenarios.find(s => s.name === name);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Save scenario to file
|
|
146
|
+
*/
|
|
147
|
+
saveScenario(scenario) {
|
|
148
|
+
if (!fs.existsSync(this.scenariosDir)) {
|
|
149
|
+
fs.mkdirSync(this.scenariosDir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
const filePath = path.join(this.scenariosDir, `${scenario.name}.json`);
|
|
152
|
+
fs.writeFileSync(filePath, JSON.stringify(scenario, null, 2), 'utf8');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Load scenario from file
|
|
156
|
+
*/
|
|
157
|
+
loadScenarioFromFile(name) {
|
|
158
|
+
const filePath = path.join(this.scenariosDir, `${name}.json`);
|
|
159
|
+
if (!fs.existsSync(filePath)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
163
|
+
return JSON.parse(content);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Convert scenario to ExecutionPlan
|
|
167
|
+
*/
|
|
168
|
+
scenarioToPlan(scenario) {
|
|
169
|
+
return {
|
|
170
|
+
goal: scenario.goal,
|
|
171
|
+
createdAt: new Date().toISOString(),
|
|
172
|
+
steps: scenario.steps,
|
|
173
|
+
metadata: {
|
|
174
|
+
totalSteps: scenario.steps.length,
|
|
175
|
+
estimatedDuration: scenario.expectedDuration
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.DemoMode = DemoMode;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.executeOptionalDeployment = executeOptionalDeployment;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const logger_1 = require("./logger");
|
|
9
|
+
const deployment_manager_1 = require("./deployment-manager");
|
|
10
|
+
const external_tool_manager_1 = require("./external-tool-manager");
|
|
11
|
+
const logger = (0, logger_1.getLogger)('deployment');
|
|
12
|
+
async function executeOptionalDeployment(workingDir, step, agent, context, options) {
|
|
13
|
+
logger.info(` 🚀 Deploying preview (--confirm-deploy)...`);
|
|
14
|
+
const toolManager = new external_tool_manager_1.ExternalToolManager({
|
|
15
|
+
enableExternal: options.enableExternal || true,
|
|
16
|
+
dryRun: options.dryRun || false,
|
|
17
|
+
logFile: path_1.default.join(context.runDir, 'deployment-commands.log')
|
|
18
|
+
});
|
|
19
|
+
const deploymentManager = new deployment_manager_1.DeploymentManager(toolManager, workingDir);
|
|
20
|
+
try {
|
|
21
|
+
const branchName = context.results.find(r => r.stepNumber === step.stepNumber)?.branchName || 'unknown';
|
|
22
|
+
// tag HEAD before deploy for rollback safety
|
|
23
|
+
let preDeployTag;
|
|
24
|
+
try {
|
|
25
|
+
preDeployTag = deploymentManager.tagPreDeploy(context.executionId);
|
|
26
|
+
logger.info(` 🏷️ Tagged: ${preDeployTag}`);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
logger.info(` [deploy] Could not tag pre-deploy (non-fatal): ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
const deployResult = await deploymentManager.deployPreview(branchName);
|
|
33
|
+
if (deployResult.success && deployResult.previewUrl) {
|
|
34
|
+
logger.info(` ✅ Preview deployed: ${deployResult.previewUrl}`);
|
|
35
|
+
// health check the preview URL
|
|
36
|
+
const healthy = await deploymentManager.runHealthCheck(deployResult.previewUrl);
|
|
37
|
+
if (!healthy && preDeployTag) {
|
|
38
|
+
logger.warn(` ⚠️ Health check failed, rolling back...`);
|
|
39
|
+
try {
|
|
40
|
+
deploymentManager.rollbackToTag(preDeployTag);
|
|
41
|
+
}
|
|
42
|
+
catch (rollbackErr) {
|
|
43
|
+
const msg = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
44
|
+
logger.warn(` ⚠️ Rollback failed: ${msg}`);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
else if (healthy) {
|
|
49
|
+
logger.info(` ✅ Health check passed`);
|
|
50
|
+
}
|
|
51
|
+
// store deployment metadata
|
|
52
|
+
const metadata = {
|
|
53
|
+
stepNumber: step.stepNumber,
|
|
54
|
+
agentName: agent.name,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
platform: deployResult.platform,
|
|
57
|
+
previewUrl: deployResult.previewUrl,
|
|
58
|
+
branchName
|
|
59
|
+
};
|
|
60
|
+
deploymentManager.saveDeploymentMetadata(context.runDir, metadata);
|
|
61
|
+
// add to context deployments
|
|
62
|
+
if (!context.deployments) {
|
|
63
|
+
context.deployments = [];
|
|
64
|
+
}
|
|
65
|
+
context.deployments.push(metadata);
|
|
66
|
+
}
|
|
67
|
+
else if (deployResult.platform === 'none') {
|
|
68
|
+
logger.info(` ℹ️ No deployment platform detected (vercel/netlify), skipping`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
logger.warn(` ⚠️ Deployment failed: ${deployResult.error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const err = error;
|
|
76
|
+
logger.warn(` ⚠️ Deployment error: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DeploymentManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const logger_1 = require("./logger");
|
|
41
|
+
const logger = (0, logger_1.getLogger)('deployment-manager');
|
|
42
|
+
/**
|
|
43
|
+
* Deployment Manager - handles preview deployments for Vercel and Netlify
|
|
44
|
+
*/
|
|
45
|
+
class DeploymentManager {
|
|
46
|
+
toolManager;
|
|
47
|
+
workingDir;
|
|
48
|
+
constructor(toolManager, workingDir) {
|
|
49
|
+
this.toolManager = toolManager;
|
|
50
|
+
this.workingDir = workingDir || process.cwd();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detect which deployment platform is configured.
|
|
54
|
+
* Config files take priority over CLI availability (project-specific > global).
|
|
55
|
+
*/
|
|
56
|
+
async detectPlatform() {
|
|
57
|
+
// 1. Check config files first (project-specific, most reliable)
|
|
58
|
+
const hasVercelConfig = fs.existsSync(path.join(this.workingDir, 'vercel.json'));
|
|
59
|
+
const hasNetlifyConfig = fs.existsSync(path.join(this.workingDir, 'netlify.toml'));
|
|
60
|
+
if (hasVercelConfig)
|
|
61
|
+
return 'vercel';
|
|
62
|
+
if (hasNetlifyConfig)
|
|
63
|
+
return 'netlify';
|
|
64
|
+
// 2. Fall back to CLI availability only when no config files present
|
|
65
|
+
const tools = await this.toolManager.detectAvailableTools();
|
|
66
|
+
if (tools.vercel)
|
|
67
|
+
return 'vercel';
|
|
68
|
+
if (tools.netlify)
|
|
69
|
+
return 'netlify';
|
|
70
|
+
return 'none';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Deploy preview for a branch
|
|
74
|
+
*/
|
|
75
|
+
async deployPreview(branchName) {
|
|
76
|
+
const platform = await this.detectPlatform();
|
|
77
|
+
if (platform === 'none') {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
platform: 'none',
|
|
81
|
+
error: 'No deployment platform detected (Vercel or Netlify)'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (platform === 'vercel') {
|
|
85
|
+
return this.deployVercelPreview(branchName);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return this.deployNetlifyPreview(branchName);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Deploy Vercel preview
|
|
93
|
+
*/
|
|
94
|
+
async deployVercelPreview(_branchName) {
|
|
95
|
+
const execution = await this.toolManager.executeCommand('vercel', ['deploy', '--yes'], {
|
|
96
|
+
workingDir: this.workingDir,
|
|
97
|
+
requireTool: 'vercel'
|
|
98
|
+
});
|
|
99
|
+
if (execution.exitCode !== 0) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
platform: 'vercel',
|
|
103
|
+
error: execution.error || 'Vercel deployment failed',
|
|
104
|
+
...(execution.output && { output: execution.output })
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Extract preview URL from Vercel output
|
|
108
|
+
const previewUrl = this.extractVercelUrl(execution.output || '');
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
platform: 'vercel',
|
|
112
|
+
...(previewUrl && { previewUrl }),
|
|
113
|
+
...(execution.output && { output: execution.output })
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Deploy Netlify preview
|
|
118
|
+
*/
|
|
119
|
+
async deployNetlifyPreview(_branchName) {
|
|
120
|
+
const execution = await this.toolManager.executeCommand('netlify', ['deploy', '--build'], {
|
|
121
|
+
workingDir: this.workingDir,
|
|
122
|
+
requireTool: 'netlify'
|
|
123
|
+
});
|
|
124
|
+
if (execution.exitCode !== 0) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
platform: 'netlify',
|
|
128
|
+
error: execution.error || 'Netlify deployment failed',
|
|
129
|
+
...(execution.output && { output: execution.output })
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Extract draft URL from Netlify output
|
|
133
|
+
const previewUrl = this.extractNetlifyUrl(execution.output || '');
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
platform: 'netlify',
|
|
137
|
+
...(previewUrl && { previewUrl }),
|
|
138
|
+
...(execution.output && { output: execution.output })
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Extract Vercel preview URL from CLI output
|
|
143
|
+
*/
|
|
144
|
+
extractVercelUrl(output) {
|
|
145
|
+
// Vercel outputs: "Preview: https://..."
|
|
146
|
+
const match = output.match(/(?:Preview|Inspect):\s+(https:\/\/[^\s]+)/i);
|
|
147
|
+
return match ? match[1] : undefined;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Extract Netlify preview URL from CLI output
|
|
151
|
+
*/
|
|
152
|
+
extractNetlifyUrl(output) {
|
|
153
|
+
// Netlify outputs: "Draft deploy URL: https://..."
|
|
154
|
+
const match = output.match(/(?:Draft\s+deploy\s+URL|Website\s+Draft\s+URL):\s+(https:\/\/[^\s]+)/i);
|
|
155
|
+
return match ? match[1] : undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Save deployment metadata to run artifacts
|
|
159
|
+
*/
|
|
160
|
+
saveDeploymentMetadata(runDir, metadata) {
|
|
161
|
+
const deploymentsDir = path.join(runDir, 'deployments');
|
|
162
|
+
if (!fs.existsSync(deploymentsDir)) {
|
|
163
|
+
fs.mkdirSync(deploymentsDir, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
const metadataPath = path.join(deploymentsDir, `step-${metadata.stepNumber}-${metadata.platform}.json`);
|
|
166
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
|
|
167
|
+
return metadataPath;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Load all deployment metadata from run
|
|
171
|
+
*/
|
|
172
|
+
loadDeploymentMetadata(runDir) {
|
|
173
|
+
const deploymentsDir = path.join(runDir, 'deployments');
|
|
174
|
+
if (!fs.existsSync(deploymentsDir)) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const files = fs.readdirSync(deploymentsDir);
|
|
178
|
+
const metadata = [];
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
if (file.endsWith('.json')) {
|
|
181
|
+
const content = fs.readFileSync(path.join(deploymentsDir, file), 'utf8');
|
|
182
|
+
metadata.push(JSON.parse(content));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return metadata.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Tag HEAD before deployment so we can roll back if the health check fails.
|
|
189
|
+
*/
|
|
190
|
+
tagPreDeploy(executionId) {
|
|
191
|
+
const tag = `pre-deploy/${executionId}`;
|
|
192
|
+
(0, child_process_1.execSync)(`git tag -f ${tag}`, { cwd: this.workingDir, encoding: 'utf8' });
|
|
193
|
+
return tag;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* HTTP health check against a preview URL. Retries on non-200 responses.
|
|
197
|
+
*/
|
|
198
|
+
async runHealthCheck(url, retries = 3, intervalMs = 20000) {
|
|
199
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
|
|
202
|
+
if (response.ok)
|
|
203
|
+
return true;
|
|
204
|
+
logger.info(` [health-check] Attempt ${attempt}/${retries}: HTTP ${response.status}`);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
logger.info(` [health-check] Attempt ${attempt}/${retries}: ${msg}`);
|
|
209
|
+
}
|
|
210
|
+
if (attempt < retries) {
|
|
211
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Roll back HEAD by reverting the latest commit and recording the event.
|
|
218
|
+
*/
|
|
219
|
+
rollbackToTag(tag) {
|
|
220
|
+
(0, child_process_1.execSync)('git revert --no-commit HEAD', { cwd: this.workingDir, encoding: 'utf8' });
|
|
221
|
+
(0, child_process_1.execSync)(`git commit -m "rollback: revert to ${tag} after failed health check"`, {
|
|
222
|
+
cwd: this.workingDir, encoding: 'utf8'
|
|
223
|
+
});
|
|
224
|
+
logger.info(` [rollback] Reverted HEAD, tagged from ${tag}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
exports.DeploymentManager = DeploymentManager;
|
|
228
|
+
exports.default = DeploymentManager;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExecutionQueue = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
/**
|
|
6
|
+
* execution queue with concurrency control and retry logic
|
|
7
|
+
* handles rate limits and quota exhaustion gracefully
|
|
8
|
+
*/
|
|
9
|
+
class ExecutionQueue extends events_1.EventEmitter {
|
|
10
|
+
queue = [];
|
|
11
|
+
running = new Map();
|
|
12
|
+
maxConcurrency;
|
|
13
|
+
retryConfig;
|
|
14
|
+
stats = {
|
|
15
|
+
completed: 0,
|
|
16
|
+
failed: 0
|
|
17
|
+
};
|
|
18
|
+
constructor(maxConcurrency = 3, retryConfig) {
|
|
19
|
+
super();
|
|
20
|
+
// High step counts generate many concurrent task-completed listeners
|
|
21
|
+
this.setMaxListeners(50);
|
|
22
|
+
this.maxConcurrency = maxConcurrency;
|
|
23
|
+
this.retryConfig = {
|
|
24
|
+
maxRetries: retryConfig?.maxRetries ?? 3,
|
|
25
|
+
baseDelay: retryConfig?.baseDelay ?? 5000,
|
|
26
|
+
maxDelay: retryConfig?.maxDelay ?? 60000,
|
|
27
|
+
rateLimitPatterns: retryConfig?.rateLimitPatterns ?? [
|
|
28
|
+
/rate limit/i,
|
|
29
|
+
/quota.*exceed/i,
|
|
30
|
+
/too many requests/i,
|
|
31
|
+
/429/,
|
|
32
|
+
/throttle/i
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* add task to queue with priority
|
|
38
|
+
*/
|
|
39
|
+
async enqueue(id, task, options) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const queuedTask = {
|
|
42
|
+
id,
|
|
43
|
+
priority: options?.priority ?? 0,
|
|
44
|
+
execute: task,
|
|
45
|
+
resolve,
|
|
46
|
+
reject,
|
|
47
|
+
retries: 0,
|
|
48
|
+
maxRetries: options?.maxRetries ?? this.retryConfig.maxRetries,
|
|
49
|
+
metadata: options?.metadata || {}
|
|
50
|
+
};
|
|
51
|
+
// insert based on priority (higher priority first)
|
|
52
|
+
const insertIndex = this.queue.findIndex(t => t.priority < queuedTask.priority);
|
|
53
|
+
if (insertIndex === -1) {
|
|
54
|
+
this.queue.push(queuedTask);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.queue.splice(insertIndex, 0, queuedTask);
|
|
58
|
+
}
|
|
59
|
+
this.emit('enqueued', { id, stats: this.getStats() });
|
|
60
|
+
this.processQueue();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* process queued tasks up to concurrency limit
|
|
65
|
+
*/
|
|
66
|
+
async processQueue() {
|
|
67
|
+
while (this.running.size < this.maxConcurrency && this.queue.length > 0) {
|
|
68
|
+
const task = this.queue.shift();
|
|
69
|
+
if (!task)
|
|
70
|
+
break;
|
|
71
|
+
this.running.set(task.id, task);
|
|
72
|
+
this.emit('started', { id: task.id, stats: this.getStats() });
|
|
73
|
+
this.executeTask(task);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* execute single task with retry on rate limits
|
|
78
|
+
*/
|
|
79
|
+
async executeTask(task) {
|
|
80
|
+
try {
|
|
81
|
+
const result = await task.execute();
|
|
82
|
+
this.running.delete(task.id);
|
|
83
|
+
this.stats.completed++;
|
|
84
|
+
this.emit('completed', { id: task.id, stats: this.getStats() });
|
|
85
|
+
task.resolve(result);
|
|
86
|
+
this.processQueue();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
90
|
+
const isRateLimit = this.retryConfig.rateLimitPatterns.some(pattern => pattern.test(errorMessage));
|
|
91
|
+
if (isRateLimit && task.retries < task.maxRetries) {
|
|
92
|
+
// exponential backoff
|
|
93
|
+
task.retries++;
|
|
94
|
+
const delay = Math.min(this.retryConfig.baseDelay * Math.pow(2, task.retries - 1), this.retryConfig.maxDelay);
|
|
95
|
+
this.emit('retrying', {
|
|
96
|
+
id: task.id,
|
|
97
|
+
attempt: task.retries,
|
|
98
|
+
maxRetries: task.maxRetries,
|
|
99
|
+
delay,
|
|
100
|
+
reason: 'rate_limit',
|
|
101
|
+
stats: this.getStats()
|
|
102
|
+
});
|
|
103
|
+
this.running.delete(task.id);
|
|
104
|
+
// re-enqueue after delay
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
this.queue.unshift(task);
|
|
107
|
+
this.processQueue();
|
|
108
|
+
}, delay);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// permanent failure
|
|
112
|
+
this.running.delete(task.id);
|
|
113
|
+
this.stats.failed++;
|
|
114
|
+
this.emit('failed', {
|
|
115
|
+
id: task.id,
|
|
116
|
+
error: errorMessage,
|
|
117
|
+
retries: task.retries,
|
|
118
|
+
stats: this.getStats()
|
|
119
|
+
});
|
|
120
|
+
task.reject(error instanceof Error ? error : new Error(errorMessage));
|
|
121
|
+
this.processQueue();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* get current queue statistics
|
|
127
|
+
*/
|
|
128
|
+
getStats() {
|
|
129
|
+
return {
|
|
130
|
+
running: this.running.size,
|
|
131
|
+
queued: this.queue.length,
|
|
132
|
+
completed: this.stats.completed,
|
|
133
|
+
failed: this.stats.failed,
|
|
134
|
+
maxConcurrency: this.maxConcurrency
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* update max concurrency dynamically
|
|
139
|
+
*/
|
|
140
|
+
setMaxConcurrency(limit) {
|
|
141
|
+
if (limit < 1) {
|
|
142
|
+
throw new Error('Max concurrency must be at least 1');
|
|
143
|
+
}
|
|
144
|
+
this.maxConcurrency = limit;
|
|
145
|
+
this.emit('concurrency_changed', { maxConcurrency: limit });
|
|
146
|
+
this.processQueue();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* pause queue processing
|
|
150
|
+
*/
|
|
151
|
+
pause() {
|
|
152
|
+
this.maxConcurrency = 0;
|
|
153
|
+
this.emit('paused');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* resume queue processing
|
|
157
|
+
*/
|
|
158
|
+
resume(concurrency) {
|
|
159
|
+
this.maxConcurrency = concurrency ?? 3;
|
|
160
|
+
this.emit('resumed', { maxConcurrency: this.maxConcurrency });
|
|
161
|
+
this.processQueue();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* clear all queued tasks
|
|
165
|
+
*/
|
|
166
|
+
clear() {
|
|
167
|
+
const cleared = this.queue.length;
|
|
168
|
+
this.queue.forEach(task => {
|
|
169
|
+
task.reject(new Error('Queue cleared'));
|
|
170
|
+
});
|
|
171
|
+
this.queue = [];
|
|
172
|
+
this.emit('cleared', { cleared });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* wait for all tasks to complete
|
|
176
|
+
*/
|
|
177
|
+
async drain() {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const checkDrained = () => {
|
|
180
|
+
if (this.running.size === 0 && this.queue.length === 0) {
|
|
181
|
+
this.removeListener('completed', checkDrained);
|
|
182
|
+
this.removeListener('failed', checkDrained);
|
|
183
|
+
resolve();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
if (this.running.size === 0 && this.queue.length === 0) {
|
|
187
|
+
resolve();
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.on('completed', checkDrained);
|
|
191
|
+
this.on('failed', checkDrained);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.ExecutionQueue = ExecutionQueue;
|