@stackweld/core 0.3.0 → 0.4.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/README.md +1 -1
- package/dist/__tests__/compatibility-scorer.test.d.ts +1 -1
- package/dist/__tests__/rules-engine.test.d.ts +1 -1
- package/dist/__tests__/scaffold-orchestrator.test.d.ts +1 -1
- package/dist/__tests__/stack-engine.test.d.ts +1 -1
- package/dist/db/database.d.ts +1 -1
- package/dist/engine/compatibility-scorer.d.ts +16 -19
- package/dist/engine/compose-generator.d.ts +21 -31
- package/dist/engine/compose-generator.js +1 -15
- package/dist/engine/cost-estimator.d.ts +13 -13
- package/dist/engine/env-analyzer.d.ts +14 -14
- package/dist/engine/health-checker.d.ts +12 -12
- package/dist/engine/health-checker.js +14 -6
- package/dist/engine/infra-generator.d.ts +14 -17
- package/dist/engine/license-manager.d.ts +29 -0
- package/dist/engine/license-manager.js +130 -0
- package/dist/engine/migration-planner.d.ts +18 -22
- package/dist/engine/performance-profiler.d.ts +13 -13
- package/dist/engine/preferences.d.ts +7 -7
- package/dist/engine/runtime-manager.d.ts +46 -56
- package/dist/engine/runtime-manager.js +24 -10
- package/dist/engine/scaffold-orchestrator.js +42 -6
- package/dist/engine/stack-detector.d.ts +10 -10
- package/dist/engine/stack-differ.d.ts +15 -15
- package/dist/engine/stack-differ.js +76 -72
- package/dist/engine/stack-engine.d.ts +0 -20
- package/dist/engine/stack-engine.js +0 -24
- package/dist/engine/stack-serializer.d.ts +8 -8
- package/dist/engine/tech-installer.d.ts +12 -12
- package/dist/engine/tech-installer.js +3 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types/project.d.ts +19 -19
- package/dist/types/project.js +1 -1
- package/dist/types/stack.d.ts +18 -18
- package/dist/types/stack.js +1 -1
- package/dist/types/technology.d.ts +28 -37
- package/dist/types/technology.js +1 -1
- package/dist/types/template.d.ts +22 -22
- package/dist/types/template.js +1 -1
- package/dist/types/validation.d.ts +12 -12
- package/dist/types/validation.js +1 -1
- package/package.json +1 -1
- package/LICENSE +0 -21
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Preferences — Persistent user preferences stored in SQLite.
|
|
3
3
|
*/
|
|
4
4
|
export interface UserPreferences {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
editor: string;
|
|
6
|
+
packageManager: string;
|
|
7
|
+
shell: string;
|
|
8
|
+
dockerMode: string;
|
|
9
|
+
defaultProfile: string;
|
|
10
|
+
theme: string;
|
|
11
11
|
}
|
|
12
12
|
/** Get all preferences, merged with defaults. */
|
|
13
13
|
export declare function getPreferences(): UserPreferences;
|
|
@@ -21,4 +21,4 @@ export declare function resetPreferences(): void;
|
|
|
21
21
|
export declare function getPreferenceKeys(): (keyof UserPreferences)[];
|
|
22
22
|
/** Get default values. */
|
|
23
23
|
export declare function getDefaultPreferences(): UserPreferences;
|
|
24
|
-
//# sourceMappingURL=preferences.d.ts.map
|
|
24
|
+
//# sourceMappingURL=preferences.d.ts.map
|
|
@@ -4,62 +4,52 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { RuntimeState, ServiceStatus, Technology } from "../types/index.js";
|
|
6
6
|
export interface RuntimeOptions {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
composePath: string;
|
|
8
|
+
projectDir: string;
|
|
9
9
|
}
|
|
10
10
|
export declare class RuntimeManager {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
opts: RuntimeOptions
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*/
|
|
55
|
-
getRuntimeState(projectId: string, opts: RuntimeOptions): RuntimeState;
|
|
56
|
-
/**
|
|
57
|
-
* Check if docker compose file exists at path.
|
|
58
|
-
*/
|
|
59
|
-
composeExists(projectDir: string): string | null;
|
|
60
|
-
/**
|
|
61
|
-
* Check if Docker is available.
|
|
62
|
-
*/
|
|
63
|
-
isDockerAvailable(): boolean;
|
|
11
|
+
private technologies;
|
|
12
|
+
constructor(technologies: Technology[]);
|
|
13
|
+
/**
|
|
14
|
+
* Start all services with docker compose up.
|
|
15
|
+
*/
|
|
16
|
+
up(opts: RuntimeOptions, detach?: boolean): {
|
|
17
|
+
success: boolean;
|
|
18
|
+
output: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Stop all services.
|
|
22
|
+
*/
|
|
23
|
+
down(opts: RuntimeOptions, volumes?: boolean): {
|
|
24
|
+
success: boolean;
|
|
25
|
+
output: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Get status of all services in the compose file.
|
|
29
|
+
*/
|
|
30
|
+
status(opts: RuntimeOptions): ServiceStatus[];
|
|
31
|
+
/**
|
|
32
|
+
* Get logs for a specific service or all services.
|
|
33
|
+
*/
|
|
34
|
+
logs(opts: RuntimeOptions, service?: string, tail?: number, follow?: boolean): string;
|
|
35
|
+
/**
|
|
36
|
+
* Wait for all services to be healthy.
|
|
37
|
+
*/
|
|
38
|
+
waitForHealthy(opts: RuntimeOptions, timeoutMs?: number, intervalMs?: number): Promise<{
|
|
39
|
+
healthy: boolean;
|
|
40
|
+
services: ServiceStatus[];
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Build RuntimeState from current container status.
|
|
44
|
+
*/
|
|
45
|
+
getRuntimeState(projectId: string, opts: RuntimeOptions): RuntimeState;
|
|
46
|
+
/**
|
|
47
|
+
* Check if docker compose file exists at path.
|
|
48
|
+
*/
|
|
49
|
+
composeExists(projectDir: string): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Check if Docker is available.
|
|
52
|
+
*/
|
|
53
|
+
isDockerAvailable(): boolean;
|
|
64
54
|
}
|
|
65
|
-
//# sourceMappingURL=runtime-manager.d.ts.map
|
|
55
|
+
//# sourceMappingURL=runtime-manager.d.ts.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Runtime Manager — Manages Docker Compose lifecycle for stacks.
|
|
3
3
|
* Handles up, down, status, logs, and health check waiting.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
export class RuntimeManager {
|
|
@@ -14,9 +14,11 @@ export class RuntimeManager {
|
|
|
14
14
|
* Start all services with docker compose up.
|
|
15
15
|
*/
|
|
16
16
|
up(opts, detach = true) {
|
|
17
|
-
const
|
|
17
|
+
const args = ["compose", "-f", opts.composePath, "up"];
|
|
18
|
+
if (detach)
|
|
19
|
+
args.push("-d");
|
|
18
20
|
try {
|
|
19
|
-
const output =
|
|
21
|
+
const output = execFileSync("docker", args, {
|
|
20
22
|
cwd: opts.projectDir,
|
|
21
23
|
stdio: "pipe",
|
|
22
24
|
timeout: 120_000,
|
|
@@ -32,9 +34,11 @@ export class RuntimeManager {
|
|
|
32
34
|
* Stop all services.
|
|
33
35
|
*/
|
|
34
36
|
down(opts, volumes = false) {
|
|
37
|
+
const args = ["compose", "-f", opts.composePath, "down"];
|
|
38
|
+
if (volumes)
|
|
39
|
+
args.push("--volumes");
|
|
35
40
|
try {
|
|
36
|
-
const
|
|
37
|
-
const output = execSync(`docker compose -f "${opts.composePath}" down${volumesFlag}`, {
|
|
41
|
+
const output = execFileSync("docker", args, {
|
|
38
42
|
cwd: opts.projectDir,
|
|
39
43
|
stdio: "pipe",
|
|
40
44
|
timeout: 60_000,
|
|
@@ -51,7 +55,7 @@ export class RuntimeManager {
|
|
|
51
55
|
*/
|
|
52
56
|
status(opts) {
|
|
53
57
|
try {
|
|
54
|
-
const raw =
|
|
58
|
+
const raw = execFileSync("docker", ["compose", "-f", opts.composePath, "ps", "--format", "json"], {
|
|
55
59
|
cwd: opts.projectDir,
|
|
56
60
|
stdio: "pipe",
|
|
57
61
|
timeout: 10_000,
|
|
@@ -112,10 +116,20 @@ export class RuntimeManager {
|
|
|
112
116
|
* Get logs for a specific service or all services.
|
|
113
117
|
*/
|
|
114
118
|
logs(opts, service, tail = 50, follow = false) {
|
|
119
|
+
if (service && !/^[a-zA-Z0-9_-]+$/.test(service)) {
|
|
120
|
+
throw new Error(`Invalid service name: ${service}`);
|
|
121
|
+
}
|
|
122
|
+
const args = ["compose", "-f", opts.composePath, "logs", "--tail", String(tail)];
|
|
123
|
+
if (follow)
|
|
124
|
+
args.push("-f");
|
|
125
|
+
if (service)
|
|
126
|
+
args.push(service);
|
|
115
127
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
return execFileSync("docker", args, {
|
|
129
|
+
cwd: opts.projectDir,
|
|
130
|
+
stdio: follow ? "inherit" : "pipe",
|
|
131
|
+
timeout: follow ? 0 : 10_000,
|
|
132
|
+
}).toString();
|
|
119
133
|
}
|
|
120
134
|
catch (err) {
|
|
121
135
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -170,7 +184,7 @@ export class RuntimeManager {
|
|
|
170
184
|
*/
|
|
171
185
|
isDockerAvailable() {
|
|
172
186
|
try {
|
|
173
|
-
|
|
187
|
+
execFileSync("docker", ["info"], { stdio: "pipe", timeout: 5_000 });
|
|
174
188
|
return true;
|
|
175
189
|
}
|
|
176
190
|
catch {
|
|
@@ -7,7 +7,41 @@
|
|
|
7
7
|
* - .gitignore (combined)
|
|
8
8
|
* - devcontainer.json
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
const WEAK_SECRETS = new Set([
|
|
13
|
+
"postgres",
|
|
14
|
+
"root",
|
|
15
|
+
"admin",
|
|
16
|
+
"password",
|
|
17
|
+
"secret",
|
|
18
|
+
"change-me",
|
|
19
|
+
"change-me-in-production",
|
|
20
|
+
"minioadmin",
|
|
21
|
+
"mariadb",
|
|
22
|
+
]);
|
|
23
|
+
function securifyEnvValue(key, value, projectName) {
|
|
24
|
+
const lower = key.toLowerCase();
|
|
25
|
+
if (WEAK_SECRETS.has(value)) {
|
|
26
|
+
if (lower.includes("user") || lower.includes("username") || lower.includes("email")) {
|
|
27
|
+
return value; // usernames are fine as defaults
|
|
28
|
+
}
|
|
29
|
+
return randomBytes(20).toString("base64url");
|
|
30
|
+
}
|
|
31
|
+
// Replace weak passwords inside DATABASE_URL strings
|
|
32
|
+
if (lower.includes("url") &&
|
|
33
|
+
(value.includes("postgres:postgres") ||
|
|
34
|
+
value.includes("root:root") ||
|
|
35
|
+
value.includes("admin:admin"))) {
|
|
36
|
+
const securePass = randomBytes(20).toString("base64url");
|
|
37
|
+
return value
|
|
38
|
+
.replace(/postgres:postgres/g, `postgres:${securePass}`)
|
|
39
|
+
.replace(/root:root/g, `root:${securePass}`)
|
|
40
|
+
.replace(/admin:admin/g, `admin:${securePass}`)
|
|
41
|
+
.replace(/\/app/g, `/${projectName.replace(/[^a-zA-Z0-9_]/g, "_")}`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
11
45
|
export class ScaffoldOrchestrator {
|
|
12
46
|
technologies;
|
|
13
47
|
constructor(technologies) {
|
|
@@ -109,9 +143,10 @@ export class ScaffoldOrchestrator {
|
|
|
109
143
|
* Generate .env.example from all technologies' envVars.
|
|
110
144
|
*/
|
|
111
145
|
generateEnvExample(stack, techs) {
|
|
146
|
+
const projectName = stack.name.replace(/\s+/g, "-").toLowerCase();
|
|
112
147
|
const lines = [
|
|
113
148
|
`# ${stack.name} — Environment Variables`,
|
|
114
|
-
`# Generated by Stackweld`,
|
|
149
|
+
`# Generated by Stackweld — rotate secrets before production`,
|
|
115
150
|
"",
|
|
116
151
|
];
|
|
117
152
|
const seen = new Set();
|
|
@@ -122,7 +157,8 @@ export class ScaffoldOrchestrator {
|
|
|
122
157
|
lines.push(`# ${tech.name}`);
|
|
123
158
|
for (const [key, value] of entries) {
|
|
124
159
|
if (!seen.has(key)) {
|
|
125
|
-
|
|
160
|
+
const secureValue = securifyEnvValue(key, value, projectName);
|
|
161
|
+
lines.push(`${key}=${secureValue}`);
|
|
126
162
|
seen.add(key);
|
|
127
163
|
}
|
|
128
164
|
}
|
|
@@ -354,11 +390,11 @@ export class ScaffoldOrchestrator {
|
|
|
354
390
|
*/
|
|
355
391
|
initGit(projectDir, stack, initialCommit = true) {
|
|
356
392
|
try {
|
|
357
|
-
|
|
393
|
+
execFileSync("git", ["init"], { cwd: projectDir, stdio: "pipe" });
|
|
358
394
|
if (initialCommit) {
|
|
359
|
-
|
|
395
|
+
execFileSync("git", ["add", "-A"], { cwd: projectDir, stdio: "pipe" });
|
|
360
396
|
const msg = `Initial commit: ${stack.name} (${stack.profile})\n\nGenerated by Stackweld`;
|
|
361
|
-
|
|
397
|
+
execFileSync("git", ["commit", "-m", msg], {
|
|
362
398
|
cwd: projectDir,
|
|
363
399
|
stdio: "pipe",
|
|
364
400
|
});
|
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
* Stack Detector — Analyze a project directory to detect its technology stack.
|
|
3
3
|
*/
|
|
4
4
|
export interface DetectedStack {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
technologies: DetectedTech[];
|
|
6
|
+
confidence: number;
|
|
7
|
+
projectType: "frontend" | "backend" | "fullstack" | "monorepo" | "library" | "unknown";
|
|
8
|
+
packageManagers: string[];
|
|
9
9
|
}
|
|
10
10
|
export interface DetectedTech {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
confidence: number;
|
|
14
|
+
detectedVia: string;
|
|
15
|
+
version?: string;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Detect the technology stack of a project at the given path.
|
|
19
19
|
*/
|
|
20
20
|
export declare function detectStack(projectPath: string): DetectedStack;
|
|
21
|
-
//# sourceMappingURL=stack-detector.d.ts.map
|
|
21
|
+
//# sourceMappingURL=stack-detector.d.ts.map
|
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { StackDefinition } from "../types/stack.js";
|
|
5
5
|
export interface StackDiff {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
added: DiffItem[];
|
|
7
|
+
removed: DiffItem[];
|
|
8
|
+
changed: DiffChange[];
|
|
9
|
+
unchanged: string[];
|
|
10
|
+
summary: string;
|
|
11
11
|
}
|
|
12
12
|
export interface DiffItem {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
technologyId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
port?: number;
|
|
17
17
|
}
|
|
18
18
|
export interface DiffChange {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
technologyId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
field: string;
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
24
|
}
|
|
25
25
|
export declare function diffStacks(stackA: StackDefinition, stackB: StackDefinition): StackDiff;
|
|
26
|
-
//# sourceMappingURL=stack-differ.d.ts.map
|
|
26
|
+
//# sourceMappingURL=stack-differ.d.ts.map
|
|
@@ -3,78 +3,82 @@
|
|
|
3
3
|
*/
|
|
4
4
|
// ─── Main Function ────────────────────────────────────
|
|
5
5
|
export function diffStacks(stackA, stackB) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
6
|
+
const techMapA = new Map(stackA.technologies.map((t) => [t.technologyId, t]));
|
|
7
|
+
const techMapB = new Map(stackB.technologies.map((t) => [t.technologyId, t]));
|
|
8
|
+
const added = [];
|
|
9
|
+
const removed = [];
|
|
10
|
+
const changed = [];
|
|
11
|
+
const unchanged = [];
|
|
12
|
+
// Find removed (in A but not B) and changed/unchanged
|
|
13
|
+
for (const [id, techA] of techMapA) {
|
|
14
|
+
const techB = techMapB.get(id);
|
|
15
|
+
if (!techB) {
|
|
16
|
+
removed.push({
|
|
17
|
+
technologyId: id,
|
|
18
|
+
name: id,
|
|
19
|
+
version: techA.version,
|
|
20
|
+
port: techA.port,
|
|
21
|
+
});
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
let hasChanges = false;
|
|
25
|
+
if (techA.version !== techB.version) {
|
|
26
|
+
changed.push({
|
|
27
|
+
technologyId: id,
|
|
28
|
+
name: id,
|
|
29
|
+
field: "version",
|
|
30
|
+
from: techA.version,
|
|
31
|
+
to: techB.version,
|
|
32
|
+
});
|
|
33
|
+
hasChanges = true;
|
|
34
|
+
}
|
|
35
|
+
if (techA.port !== techB.port) {
|
|
36
|
+
changed.push({
|
|
37
|
+
technologyId: id,
|
|
38
|
+
name: id,
|
|
39
|
+
field: "port",
|
|
40
|
+
from: techA.port !== undefined ? String(techA.port) : "none",
|
|
41
|
+
to: techB.port !== undefined ? String(techB.port) : "none",
|
|
42
|
+
});
|
|
43
|
+
hasChanges = true;
|
|
44
|
+
}
|
|
45
|
+
const configA = JSON.stringify(techA.config ?? {});
|
|
46
|
+
const configB = JSON.stringify(techB.config ?? {});
|
|
47
|
+
if (configA !== configB) {
|
|
48
|
+
changed.push({
|
|
49
|
+
technologyId: id,
|
|
50
|
+
name: id,
|
|
51
|
+
field: "config",
|
|
52
|
+
from: configA,
|
|
53
|
+
to: configB,
|
|
54
|
+
});
|
|
55
|
+
hasChanges = true;
|
|
56
|
+
}
|
|
57
|
+
if (!hasChanges) {
|
|
58
|
+
unchanged.push(id);
|
|
59
|
+
}
|
|
23
60
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
61
|
+
// Find added (in B but not A)
|
|
62
|
+
for (const [id, techB] of techMapB) {
|
|
63
|
+
if (!techMapA.has(id)) {
|
|
64
|
+
added.push({
|
|
65
|
+
technologyId: id,
|
|
66
|
+
name: id,
|
|
67
|
+
version: techB.version,
|
|
68
|
+
port: techB.port,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
34
71
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const configB = JSON.stringify(techB.config ?? {});
|
|
47
|
-
if (configA !== configB) {
|
|
48
|
-
changed.push({
|
|
49
|
-
technologyId: id,
|
|
50
|
-
name: id,
|
|
51
|
-
field: "config",
|
|
52
|
-
from: configA,
|
|
53
|
-
to: configB,
|
|
54
|
-
});
|
|
55
|
-
hasChanges = true;
|
|
56
|
-
}
|
|
57
|
-
if (!hasChanges) {
|
|
58
|
-
unchanged.push(id);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Find added (in B but not A)
|
|
62
|
-
for (const [id, techB] of techMapB) {
|
|
63
|
-
if (!techMapA.has(id)) {
|
|
64
|
-
added.push({
|
|
65
|
-
technologyId: id,
|
|
66
|
-
name: id,
|
|
67
|
-
version: techB.version,
|
|
68
|
-
port: techB.port,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const parts = [];
|
|
73
|
-
if (added.length > 0) parts.push(`+${added.length} added`);
|
|
74
|
-
if (removed.length > 0) parts.push(`-${removed.length} removed`);
|
|
75
|
-
if (changed.length > 0) parts.push(`~${changed.length} changed`);
|
|
76
|
-
if (unchanged.length > 0) parts.push(`${unchanged.length} unchanged`);
|
|
77
|
-
const summary = parts.join(", ");
|
|
78
|
-
return { added, removed, changed, unchanged, summary };
|
|
72
|
+
const parts = [];
|
|
73
|
+
if (added.length > 0)
|
|
74
|
+
parts.push(`+${added.length} added`);
|
|
75
|
+
if (removed.length > 0)
|
|
76
|
+
parts.push(`-${removed.length} removed`);
|
|
77
|
+
if (changed.length > 0)
|
|
78
|
+
parts.push(`~${changed.length} changed`);
|
|
79
|
+
if (unchanged.length > 0)
|
|
80
|
+
parts.push(`${unchanged.length} unchanged`);
|
|
81
|
+
const summary = parts.join(", ");
|
|
82
|
+
return { added, removed, changed, unchanged, summary };
|
|
79
83
|
}
|
|
80
|
-
//# sourceMappingURL=stack-differ.js.map
|
|
84
|
+
//# sourceMappingURL=stack-differ.js.map
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stack Engine — CRUD operations for stack definitions.
|
|
3
|
-
* Handles creation, validation, versioning, and persistence.
|
|
4
|
-
*/
|
|
5
1
|
import type { StackDefinition, StackProfile, StackTechnology, StackVersion, ValidationResult } from "../types/index.js";
|
|
6
2
|
import type { RulesEngine } from "./rules-engine.js";
|
|
7
3
|
export declare class StackEngine {
|
|
8
4
|
private rules;
|
|
9
5
|
constructor(rules: RulesEngine);
|
|
10
|
-
/**
|
|
11
|
-
* Create a new stack definition.
|
|
12
|
-
* Validates technologies and auto-resolves dependencies.
|
|
13
|
-
*/
|
|
14
6
|
create(opts: {
|
|
15
7
|
name: string;
|
|
16
8
|
description?: string;
|
|
@@ -21,21 +13,9 @@ export declare class StackEngine {
|
|
|
21
13
|
stack: StackDefinition;
|
|
22
14
|
validation: ValidationResult;
|
|
23
15
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Get a stack by ID.
|
|
26
|
-
*/
|
|
27
16
|
get(id: string): StackDefinition | null;
|
|
28
|
-
/**
|
|
29
|
-
* List all stacks.
|
|
30
|
-
*/
|
|
31
17
|
list(): StackDefinition[];
|
|
32
|
-
/**
|
|
33
|
-
* Delete a stack.
|
|
34
|
-
*/
|
|
35
18
|
delete(id: string): boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Update a stack. Auto-increments version and saves snapshot.
|
|
38
|
-
*/
|
|
39
19
|
update(id: string, changes: Partial<Pick<StackDefinition, "name" | "description" | "profile" | "technologies" | "tags">>): {
|
|
40
20
|
stack: StackDefinition;
|
|
41
21
|
validation: ValidationResult;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stack Engine — CRUD operations for stack definitions.
|
|
3
|
-
* Handles creation, validation, versioning, and persistence.
|
|
4
|
-
*/
|
|
5
1
|
import { randomUUID } from "node:crypto";
|
|
6
2
|
import { getDatabase } from "../db/database.js";
|
|
7
3
|
export class StackEngine {
|
|
@@ -9,13 +5,8 @@ export class StackEngine {
|
|
|
9
5
|
constructor(rules) {
|
|
10
6
|
this.rules = rules;
|
|
11
7
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Create a new stack definition.
|
|
14
|
-
* Validates technologies and auto-resolves dependencies.
|
|
15
|
-
*/
|
|
16
8
|
create(opts) {
|
|
17
9
|
const validation = this.rules.validate(opts.technologies);
|
|
18
|
-
// Add auto-resolved dependencies
|
|
19
10
|
const allTechs = [...opts.technologies];
|
|
20
11
|
for (const depId of validation.resolvedDependencies) {
|
|
21
12
|
const tech = this.rules.getTechnology(depId);
|
|
@@ -27,7 +18,6 @@ export class StackEngine {
|
|
|
27
18
|
});
|
|
28
19
|
}
|
|
29
20
|
}
|
|
30
|
-
// Apply port assignments to existing techs
|
|
31
21
|
for (const t of allTechs) {
|
|
32
22
|
if (validation.portAssignments[t.technologyId]) {
|
|
33
23
|
t.port = validation.portAssignments[t.technologyId];
|
|
@@ -50,9 +40,6 @@ export class StackEngine {
|
|
|
50
40
|
}
|
|
51
41
|
return { stack, validation };
|
|
52
42
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Get a stack by ID.
|
|
55
|
-
*/
|
|
56
43
|
get(id) {
|
|
57
44
|
const db = getDatabase();
|
|
58
45
|
const row = db.prepare("SELECT * FROM stacks WHERE id = ?").get(id);
|
|
@@ -78,25 +65,16 @@ export class StackEngine {
|
|
|
78
65
|
})),
|
|
79
66
|
};
|
|
80
67
|
}
|
|
81
|
-
/**
|
|
82
|
-
* List all stacks.
|
|
83
|
-
*/
|
|
84
68
|
list() {
|
|
85
69
|
const db = getDatabase();
|
|
86
70
|
const rows = db.prepare("SELECT id FROM stacks ORDER BY updated_at DESC").all();
|
|
87
71
|
return rows.map((r) => this.get(r.id)).filter(Boolean);
|
|
88
72
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Delete a stack.
|
|
91
|
-
*/
|
|
92
73
|
delete(id) {
|
|
93
74
|
const db = getDatabase();
|
|
94
75
|
const result = db.prepare("DELETE FROM stacks WHERE id = ?").run(id);
|
|
95
76
|
return result.changes > 0;
|
|
96
77
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Update a stack. Auto-increments version and saves snapshot.
|
|
99
|
-
*/
|
|
100
78
|
update(id, changes) {
|
|
101
79
|
const existing = this.get(id);
|
|
102
80
|
if (!existing)
|
|
@@ -112,7 +90,6 @@ export class StackEngine {
|
|
|
112
90
|
updated_at = ?, tags = ?
|
|
113
91
|
WHERE id = ?
|
|
114
92
|
`).run(changes.name || existing.name, changes.description ?? existing.description, changes.profile || existing.profile, newVersion, now, JSON.stringify(changes.tags || existing.tags), id);
|
|
115
|
-
// Replace technologies
|
|
116
93
|
db.prepare("DELETE FROM stack_technologies WHERE stack_id = ?").run(id);
|
|
117
94
|
for (const t of techs) {
|
|
118
95
|
db.prepare(`
|
|
@@ -120,7 +97,6 @@ export class StackEngine {
|
|
|
120
97
|
VALUES (?, ?, ?, ?, ?)
|
|
121
98
|
`).run(id, t.technologyId, t.version, t.port || null, JSON.stringify(t.config || {}));
|
|
122
99
|
}
|
|
123
|
-
// Save version snapshot
|
|
124
100
|
const updatedStack = this.get(id);
|
|
125
101
|
this.saveVersionSnapshot(updatedStack, `Updated to v${newVersion}`);
|
|
126
102
|
return { stack: updatedStack, validation };
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Uses zlib deflate + base64url encoding. No server needed.
|
|
4
4
|
*/
|
|
5
5
|
export interface ShareableStack {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
name: string;
|
|
7
|
+
profile: string;
|
|
8
|
+
technologies: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
port?: number;
|
|
12
|
+
}>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Compress a stack into a URL-safe base64 string.
|
|
@@ -29,4 +29,4 @@ export declare function generateShareUrl(stack: ShareableStack, baseUrl?: string
|
|
|
29
29
|
* Extract the encoded payload from a share URL.
|
|
30
30
|
*/
|
|
31
31
|
export declare function extractFromShareUrl(url: string): string;
|
|
32
|
-
//# sourceMappingURL=stack-serializer.d.ts.map
|
|
32
|
+
//# sourceMappingURL=stack-serializer.d.ts.map
|