@stackweld/core 0.2.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +498 -0
- package/.turbo/turbo-test.log +21 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts +2 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts.map +1 -0
- package/dist/__tests__/compatibility-scorer.test.js +226 -0
- package/dist/__tests__/compatibility-scorer.test.js.map +1 -0
- package/dist/__tests__/rules-engine.test.d.ts +2 -0
- package/dist/__tests__/rules-engine.test.d.ts.map +1 -0
- package/dist/__tests__/rules-engine.test.js +161 -0
- package/dist/__tests__/rules-engine.test.js.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts +2 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.js +149 -0
- package/dist/__tests__/scaffold-orchestrator.test.js.map +1 -0
- package/dist/__tests__/stack-engine.test.d.ts +2 -0
- package/dist/__tests__/stack-engine.test.d.ts.map +1 -0
- package/dist/__tests__/stack-engine.test.js +278 -0
- package/dist/__tests__/stack-engine.test.js.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +106 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/engine/compatibility-scorer.d.ts +37 -0
- package/dist/engine/compatibility-scorer.d.ts.map +1 -0
- package/dist/engine/compatibility-scorer.js +178 -0
- package/dist/engine/compatibility-scorer.js.map +1 -0
- package/dist/engine/compose-generator.d.ts +35 -0
- package/dist/engine/compose-generator.d.ts.map +1 -0
- package/dist/engine/compose-generator.js +95 -0
- package/dist/engine/compose-generator.js.map +1 -0
- package/dist/engine/cost-estimator.d.ts +22 -0
- package/dist/engine/cost-estimator.d.ts.map +1 -0
- package/dist/engine/cost-estimator.js +451 -0
- package/dist/engine/cost-estimator.js.map +1 -0
- package/dist/engine/env-analyzer.d.ts +36 -0
- package/dist/engine/env-analyzer.d.ts.map +1 -0
- package/dist/engine/env-analyzer.js +111 -0
- package/dist/engine/env-analyzer.js.map +1 -0
- package/dist/engine/health-checker.d.ts +20 -0
- package/dist/engine/health-checker.d.ts.map +1 -0
- package/dist/engine/health-checker.js +377 -0
- package/dist/engine/health-checker.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/infra-generator.d.ts +26 -0
- package/dist/engine/infra-generator.d.ts.map +1 -0
- package/dist/engine/infra-generator.js +751 -0
- package/dist/engine/infra-generator.js.map +1 -0
- package/dist/engine/migration-planner.d.ts +34 -0
- package/dist/engine/migration-planner.d.ts.map +1 -0
- package/dist/engine/migration-planner.js +427 -0
- package/dist/engine/migration-planner.js.map +1 -0
- package/dist/engine/performance-profiler.d.ts +22 -0
- package/dist/engine/performance-profiler.d.ts.map +1 -0
- package/dist/engine/performance-profiler.js +292 -0
- package/dist/engine/performance-profiler.js.map +1 -0
- package/dist/engine/plugin-loader.d.ts +36 -0
- package/dist/engine/plugin-loader.d.ts.map +1 -0
- package/dist/engine/plugin-loader.js +157 -0
- package/dist/engine/plugin-loader.js.map +1 -0
- package/dist/engine/preferences.d.ts +24 -0
- package/dist/engine/preferences.d.ts.map +1 -0
- package/dist/engine/preferences.js +62 -0
- package/dist/engine/preferences.js.map +1 -0
- package/dist/engine/rules-engine.d.ts +31 -0
- package/dist/engine/rules-engine.d.ts.map +1 -0
- package/dist/engine/rules-engine.js +179 -0
- package/dist/engine/rules-engine.js.map +1 -0
- package/dist/engine/runtime-manager.d.ts +65 -0
- package/dist/engine/runtime-manager.d.ts.map +1 -0
- package/dist/engine/runtime-manager.js +181 -0
- package/dist/engine/runtime-manager.js.map +1 -0
- package/dist/engine/scaffold-orchestrator.d.ts +103 -0
- package/dist/engine/scaffold-orchestrator.d.ts.map +1 -0
- package/dist/engine/scaffold-orchestrator.js +934 -0
- package/dist/engine/scaffold-orchestrator.js.map +1 -0
- package/dist/engine/stack-detector.d.ts +21 -0
- package/dist/engine/stack-detector.d.ts.map +1 -0
- package/dist/engine/stack-detector.js +313 -0
- package/dist/engine/stack-detector.js.map +1 -0
- package/dist/engine/stack-differ.d.ts +26 -0
- package/dist/engine/stack-differ.d.ts.map +1 -0
- package/dist/engine/stack-differ.js +80 -0
- package/dist/engine/stack-differ.js.map +1 -0
- package/dist/engine/stack-engine.d.ts +54 -0
- package/dist/engine/stack-engine.d.ts.map +1 -0
- package/dist/engine/stack-engine.js +186 -0
- package/dist/engine/stack-engine.js.map +1 -0
- package/dist/engine/stack-serializer.d.ts +32 -0
- package/dist/engine/stack-serializer.d.ts.map +1 -0
- package/dist/engine/stack-serializer.js +75 -0
- package/dist/engine/stack-serializer.js.map +1 -0
- package/dist/engine/standards-linter.d.ts +34 -0
- package/dist/engine/standards-linter.d.ts.map +1 -0
- package/dist/engine/standards-linter.js +162 -0
- package/dist/engine/standards-linter.js.map +1 -0
- package/dist/engine/tech-installer.d.ts +37 -0
- package/dist/engine/tech-installer.d.ts.map +1 -0
- package/dist/engine/tech-installer.js +508 -0
- package/dist/engine/tech-installer.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +33 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +6 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/stack.d.ts +29 -0
- package/dist/types/stack.d.ts.map +1 -0
- package/dist/types/stack.js +6 -0
- package/dist/types/stack.js.map +1 -0
- package/dist/types/technology.d.ts +47 -0
- package/dist/types/technology.d.ts.map +1 -0
- package/dist/types/technology.js +6 -0
- package/dist/types/technology.js.map +1 -0
- package/dist/types/template.d.ts +34 -0
- package/dist/types/template.d.ts.map +1 -0
- package/dist/types/template.js +6 -0
- package/dist/types/template.js.map +1 -0
- package/dist/types/validation.d.ts +20 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/__tests__/compatibility-scorer.test.ts +264 -0
- package/src/__tests__/rules-engine.test.ts +170 -0
- package/src/__tests__/scaffold-orchestrator.test.ts +161 -0
- package/src/__tests__/stack-engine.test.ts +328 -0
- package/src/db/database.ts +112 -0
- package/src/db/index.ts +1 -0
- package/src/engine/compatibility-scorer.ts +222 -0
- package/src/engine/compose-generator.ts +134 -0
- package/src/engine/cost-estimator.ts +498 -0
- package/src/engine/env-analyzer.ts +156 -0
- package/src/engine/health-checker.ts +421 -0
- package/src/engine/index.ts +17 -0
- package/src/engine/infra-generator.ts +837 -0
- package/src/engine/migration-planner.ts +496 -0
- package/src/engine/performance-profiler.ts +354 -0
- package/src/engine/plugin-loader.ts +216 -0
- package/src/engine/preferences.ts +85 -0
- package/src/engine/rules-engine.ts +204 -0
- package/src/engine/runtime-manager.ts +207 -0
- package/src/engine/scaffold-orchestrator.ts +1052 -0
- package/src/engine/stack-detector.ts +345 -0
- package/src/engine/stack-differ.ts +118 -0
- package/src/engine/stack-engine.ts +258 -0
- package/src/engine/stack-serializer.ts +95 -0
- package/src/engine/standards-linter.ts +210 -0
- package/src/engine/tech-installer.ts +650 -0
- package/src/index.ts +78 -0
- package/src/types/index.ts +10 -0
- package/src/types/project.ts +36 -0
- package/src/types/stack.ts +32 -0
- package/src/types/technology.ts +58 -0
- package/src/types/template.ts +37 -0
- package/src/types/validation.ts +22 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules Engine — Validates technology compatibility.
|
|
3
|
+
* Source of truth for what can coexist in a stack.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
StackTechnology,
|
|
8
|
+
Technology,
|
|
9
|
+
ValidationIssue,
|
|
10
|
+
ValidationResult,
|
|
11
|
+
} from "../types/index.js";
|
|
12
|
+
|
|
13
|
+
export class RulesEngine {
|
|
14
|
+
private techMap: Map<string, Technology>;
|
|
15
|
+
|
|
16
|
+
constructor(technologies: Technology[]) {
|
|
17
|
+
this.techMap = new Map(technologies.map((t) => [t.id, t]));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a set of selected technologies.
|
|
22
|
+
* Returns issues, auto-resolved dependencies, and port assignments.
|
|
23
|
+
*/
|
|
24
|
+
validate(selected: StackTechnology[]): ValidationResult {
|
|
25
|
+
const issues: ValidationIssue[] = [];
|
|
26
|
+
const resolvedDependencies: string[] = [];
|
|
27
|
+
const portAssignments: Record<string, number> = {};
|
|
28
|
+
const usedPorts = new Set<number>();
|
|
29
|
+
|
|
30
|
+
const selectedIds = new Set(selected.map((s) => s.technologyId));
|
|
31
|
+
|
|
32
|
+
// 1. Check each technology exists in the registry
|
|
33
|
+
for (const sel of selected) {
|
|
34
|
+
if (!this.techMap.has(sel.technologyId)) {
|
|
35
|
+
issues.push({
|
|
36
|
+
severity: "error",
|
|
37
|
+
code: "UNKNOWN_TECHNOLOGY",
|
|
38
|
+
message: `Technology "${sel.technologyId}" not found in the registry`,
|
|
39
|
+
technologyId: sel.technologyId,
|
|
40
|
+
autoFixable: false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Check requires — resolve missing dependencies RECURSIVELY
|
|
46
|
+
// Use a queue to process newly resolved dependencies until no more are needed
|
|
47
|
+
const toProcess = [...selectedIds];
|
|
48
|
+
const processed = new Set<string>();
|
|
49
|
+
|
|
50
|
+
while (toProcess.length > 0) {
|
|
51
|
+
const currentId = toProcess.shift()!;
|
|
52
|
+
if (processed.has(currentId)) continue;
|
|
53
|
+
processed.add(currentId);
|
|
54
|
+
|
|
55
|
+
const tech = this.techMap.get(currentId);
|
|
56
|
+
if (!tech) continue;
|
|
57
|
+
|
|
58
|
+
for (const reqId of tech.requires) {
|
|
59
|
+
if (!selectedIds.has(reqId)) {
|
|
60
|
+
const reqTech = this.techMap.get(reqId);
|
|
61
|
+
if (reqTech) {
|
|
62
|
+
resolvedDependencies.push(reqId);
|
|
63
|
+
selectedIds.add(reqId);
|
|
64
|
+
// Queue the newly added dependency so its own requires are checked
|
|
65
|
+
toProcess.push(reqId);
|
|
66
|
+
issues.push({
|
|
67
|
+
severity: "info",
|
|
68
|
+
code: "AUTO_DEPENDENCY",
|
|
69
|
+
message: `"${tech.name}" requires "${reqTech.name}" — added automatically`,
|
|
70
|
+
technologyId: currentId,
|
|
71
|
+
relatedTechnologyId: reqId,
|
|
72
|
+
autoFixable: true,
|
|
73
|
+
suggestedFix: `Add ${reqTech.name} to the stack`,
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
issues.push({
|
|
77
|
+
severity: "error",
|
|
78
|
+
code: "MISSING_DEPENDENCY",
|
|
79
|
+
message: `"${tech.name}" requires "${reqId}" which is not in the registry`,
|
|
80
|
+
technologyId: currentId,
|
|
81
|
+
relatedTechnologyId: reqId,
|
|
82
|
+
autoFixable: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3. Check incompatibleWith BIDIRECTIONALLY
|
|
90
|
+
// Track reported pairs to avoid duplicate issues (A-B and B-A)
|
|
91
|
+
const reportedPairs = new Set<string>();
|
|
92
|
+
const allIds = [...selectedIds];
|
|
93
|
+
|
|
94
|
+
for (const techId of allIds) {
|
|
95
|
+
const tech = this.techMap.get(techId);
|
|
96
|
+
if (!tech) continue;
|
|
97
|
+
|
|
98
|
+
for (const otherId of allIds) {
|
|
99
|
+
if (techId === otherId) continue;
|
|
100
|
+
|
|
101
|
+
const pairKey = [techId, otherId].sort().join(":");
|
|
102
|
+
if (reportedPairs.has(pairKey)) continue;
|
|
103
|
+
|
|
104
|
+
const otherTech = this.techMap.get(otherId);
|
|
105
|
+
if (!otherTech) continue;
|
|
106
|
+
|
|
107
|
+
// Bidirectional: incompatible if EITHER side lists the other
|
|
108
|
+
const aListsB = tech.incompatibleWith.includes(otherId);
|
|
109
|
+
const bListsA = otherTech.incompatibleWith.includes(techId);
|
|
110
|
+
|
|
111
|
+
if (aListsB || bListsA) {
|
|
112
|
+
reportedPairs.add(pairKey);
|
|
113
|
+
issues.push({
|
|
114
|
+
severity: "error",
|
|
115
|
+
code: "INCOMPATIBLE",
|
|
116
|
+
message: `"${tech.name}" is incompatible with "${otherTech.name}"`,
|
|
117
|
+
technologyId: techId,
|
|
118
|
+
relatedTechnologyId: otherId,
|
|
119
|
+
autoFixable: false,
|
|
120
|
+
suggestedFix: `Remove either "${tech.name}" or "${otherTech.name}" from the stack`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. Assign ports deterministically — sort by ID for stable ordering
|
|
127
|
+
const sortedTechIds = [...selectedIds].sort();
|
|
128
|
+
for (const techId of sortedTechIds) {
|
|
129
|
+
const tech = this.techMap.get(techId);
|
|
130
|
+
if (!tech?.defaultPort) continue;
|
|
131
|
+
|
|
132
|
+
let port = tech.defaultPort;
|
|
133
|
+
|
|
134
|
+
// Find a free port if default is taken
|
|
135
|
+
if (usedPorts.has(port)) {
|
|
136
|
+
const originalPort = port;
|
|
137
|
+
while (usedPorts.has(port)) {
|
|
138
|
+
port++;
|
|
139
|
+
}
|
|
140
|
+
issues.push({
|
|
141
|
+
severity: "warning",
|
|
142
|
+
code: "PORT_CONFLICT",
|
|
143
|
+
message: `Port ${originalPort} for "${tech.name}" conflicts — reassigned to ${port}`,
|
|
144
|
+
technologyId: techId,
|
|
145
|
+
autoFixable: true,
|
|
146
|
+
suggestedFix: `Using port ${port} instead of ${originalPort}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
usedPorts.add(port);
|
|
151
|
+
portAssignments[techId] = port;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
valid: !hasErrors,
|
|
158
|
+
issues,
|
|
159
|
+
resolvedDependencies,
|
|
160
|
+
portAssignments,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get suggested technologies for a given set.
|
|
166
|
+
*/
|
|
167
|
+
getSuggestions(selectedIds: string[]): Technology[] {
|
|
168
|
+
const selected = new Set(selectedIds);
|
|
169
|
+
const suggestions = new Set<string>();
|
|
170
|
+
|
|
171
|
+
for (const id of selectedIds) {
|
|
172
|
+
const tech = this.techMap.get(id);
|
|
173
|
+
if (!tech) continue;
|
|
174
|
+
for (const sugId of tech.suggestedWith) {
|
|
175
|
+
if (!selected.has(sugId) && this.techMap.has(sugId)) {
|
|
176
|
+
suggestions.add(sugId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [...suggestions].map((id) => this.techMap.get(id)!);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get a technology by ID.
|
|
186
|
+
*/
|
|
187
|
+
getTechnology(id: string): Technology | undefined {
|
|
188
|
+
return this.techMap.get(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all technologies.
|
|
193
|
+
*/
|
|
194
|
+
getAllTechnologies(): Technology[] {
|
|
195
|
+
return [...this.techMap.values()];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get technologies by category.
|
|
200
|
+
*/
|
|
201
|
+
getByCategory(category: string): Technology[] {
|
|
202
|
+
return [...this.techMap.values()].filter((t) => t.category === category);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Manager — Manages Docker Compose lifecycle for stacks.
|
|
3
|
+
* Handles up, down, status, logs, and health check waiting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { RuntimeState, ServiceStatus, Technology } from "../types/index.js";
|
|
10
|
+
|
|
11
|
+
export interface RuntimeOptions {
|
|
12
|
+
composePath: string;
|
|
13
|
+
projectDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class RuntimeManager {
|
|
17
|
+
private technologies: Map<string, Technology>;
|
|
18
|
+
|
|
19
|
+
constructor(technologies: Technology[]) {
|
|
20
|
+
this.technologies = new Map(technologies.map((t) => [t.id, t]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Start all services with docker compose up.
|
|
25
|
+
*/
|
|
26
|
+
up(opts: RuntimeOptions, detach = true): { success: boolean; output: string } {
|
|
27
|
+
const flags = detach ? "-d" : "";
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(`docker compose -f "${opts.composePath}" up ${flags}`, {
|
|
30
|
+
cwd: opts.projectDir,
|
|
31
|
+
stdio: "pipe",
|
|
32
|
+
timeout: 120_000,
|
|
33
|
+
}).toString();
|
|
34
|
+
return { success: true, output };
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
return { success: false, output: message };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stop all services.
|
|
43
|
+
*/
|
|
44
|
+
down(opts: RuntimeOptions, volumes = false): { success: boolean; output: string } {
|
|
45
|
+
try {
|
|
46
|
+
const volumesFlag = volumes ? " --volumes" : "";
|
|
47
|
+
const output = execSync(`docker compose -f "${opts.composePath}" down${volumesFlag}`, {
|
|
48
|
+
cwd: opts.projectDir,
|
|
49
|
+
stdio: "pipe",
|
|
50
|
+
timeout: 60_000,
|
|
51
|
+
}).toString();
|
|
52
|
+
return { success: true, output };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
+
return { success: false, output: message };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get status of all services in the compose file.
|
|
61
|
+
*/
|
|
62
|
+
status(opts: RuntimeOptions): ServiceStatus[] {
|
|
63
|
+
try {
|
|
64
|
+
const raw = execSync(`docker compose -f "${opts.composePath}" ps --format json`, {
|
|
65
|
+
cwd: opts.projectDir,
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
timeout: 10_000,
|
|
68
|
+
}).toString();
|
|
69
|
+
|
|
70
|
+
const services: ServiceStatus[] = [];
|
|
71
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
72
|
+
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
try {
|
|
75
|
+
const container = JSON.parse(line);
|
|
76
|
+
const serviceName = container.Service || container.Name || "unknown";
|
|
77
|
+
const state = container.State || "unknown";
|
|
78
|
+
const health = container.Health || "";
|
|
79
|
+
|
|
80
|
+
// Try to find matching technology
|
|
81
|
+
const tech = this.technologies.get(serviceName);
|
|
82
|
+
|
|
83
|
+
let statusValue: ServiceStatus["status"];
|
|
84
|
+
if (state === "running" && health === "healthy") {
|
|
85
|
+
statusValue = "healthy";
|
|
86
|
+
} else if (state === "running" && health === "unhealthy") {
|
|
87
|
+
statusValue = "unhealthy";
|
|
88
|
+
} else if (state === "running") {
|
|
89
|
+
statusValue = "running";
|
|
90
|
+
} else if (state === "exited") {
|
|
91
|
+
statusValue = "exited";
|
|
92
|
+
} else {
|
|
93
|
+
statusValue = "stopped";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract port from Publishers
|
|
97
|
+
let port: number | undefined;
|
|
98
|
+
if (container.Publishers && Array.isArray(container.Publishers)) {
|
|
99
|
+
const pub = container.Publishers.find((p: Record<string, unknown>) => p.PublishedPort);
|
|
100
|
+
if (pub) port = pub.PublishedPort as number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
services.push({
|
|
104
|
+
name: serviceName,
|
|
105
|
+
technologyId: tech?.id || serviceName,
|
|
106
|
+
containerId: container.ID,
|
|
107
|
+
status: statusValue,
|
|
108
|
+
port,
|
|
109
|
+
healthCheck:
|
|
110
|
+
health === "healthy" ? "passing" : health === "unhealthy" ? "failing" : "none",
|
|
111
|
+
});
|
|
112
|
+
} catch {
|
|
113
|
+
// Skip malformed JSON lines
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return services;
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get logs for a specific service or all services.
|
|
125
|
+
*/
|
|
126
|
+
logs(opts: RuntimeOptions, service?: string, tail = 50, follow = false): string {
|
|
127
|
+
try {
|
|
128
|
+
const serviceArg = service || "";
|
|
129
|
+
const followFlag = follow ? " -f" : "";
|
|
130
|
+
return execSync(
|
|
131
|
+
`docker compose -f "${opts.composePath}" logs --tail ${tail}${followFlag} ${serviceArg}`,
|
|
132
|
+
{ cwd: opts.projectDir, stdio: follow ? "inherit" : "pipe", timeout: follow ? 0 : 10_000 },
|
|
133
|
+
).toString();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return err instanceof Error ? err.message : String(err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wait for all services to be healthy.
|
|
141
|
+
*/
|
|
142
|
+
async waitForHealthy(
|
|
143
|
+
opts: RuntimeOptions,
|
|
144
|
+
timeoutMs = 60_000,
|
|
145
|
+
intervalMs = 2_000,
|
|
146
|
+
): Promise<{ healthy: boolean; services: ServiceStatus[] }> {
|
|
147
|
+
const start = Date.now();
|
|
148
|
+
|
|
149
|
+
while (Date.now() - start < timeoutMs) {
|
|
150
|
+
const services = this.status(opts);
|
|
151
|
+
const hasServices = services.length > 0;
|
|
152
|
+
const allHealthy = services.every(
|
|
153
|
+
(s) => s.status === "healthy" || s.status === "running" || s.healthCheck === "none",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (hasServices && allHealthy) {
|
|
157
|
+
return { healthy: true, services };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for exited containers (failed)
|
|
161
|
+
const failed = services.filter((s) => s.status === "exited");
|
|
162
|
+
if (failed.length > 0) {
|
|
163
|
+
return { healthy: false, services };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { healthy: false, services: this.status(opts) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build RuntimeState from current container status.
|
|
174
|
+
*/
|
|
175
|
+
getRuntimeState(projectId: string, opts: RuntimeOptions): RuntimeState {
|
|
176
|
+
return {
|
|
177
|
+
projectId,
|
|
178
|
+
services: this.status(opts),
|
|
179
|
+
composePath: opts.composePath,
|
|
180
|
+
lastChecked: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if docker compose file exists at path.
|
|
186
|
+
*/
|
|
187
|
+
composeExists(projectDir: string): string | null {
|
|
188
|
+
const candidates = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
189
|
+
for (const name of candidates) {
|
|
190
|
+
const p = path.join(projectDir, name);
|
|
191
|
+
if (fs.existsSync(p)) return p;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if Docker is available.
|
|
198
|
+
*/
|
|
199
|
+
isDockerAvailable(): boolean {
|
|
200
|
+
try {
|
|
201
|
+
execSync("docker info", { stdio: "pipe", timeout: 5_000 });
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|