@better-openclaw/core 1.0.30 → 1.0.31
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 +2 -1
- package/dist/deployers/coolify.test.cjs +156 -0
- package/dist/deployers/coolify.test.cjs.map +1 -0
- package/dist/deployers/coolify.test.d.cts +1 -0
- package/dist/deployers/coolify.test.d.mts +1 -0
- package/dist/deployers/coolify.test.mjs +157 -0
- package/dist/deployers/coolify.test.mjs.map +1 -0
- package/dist/deployers/dokploy.test.cjs +108 -0
- package/dist/deployers/dokploy.test.cjs.map +1 -0
- package/dist/deployers/dokploy.test.d.cts +1 -0
- package/dist/deployers/dokploy.test.d.mts +1 -0
- package/dist/deployers/dokploy.test.mjs +109 -0
- package/dist/deployers/dokploy.test.mjs.map +1 -0
- package/dist/frameworks/frameworks.test.cjs +94 -0
- package/dist/frameworks/frameworks.test.cjs.map +1 -0
- package/dist/frameworks/frameworks.test.d.cts +1 -0
- package/dist/frameworks/frameworks.test.d.mts +1 -0
- package/dist/frameworks/frameworks.test.mjs +94 -0
- package/dist/frameworks/frameworks.test.mjs.map +1 -0
- package/dist/generators/cloud-init.test.cjs +58 -0
- package/dist/generators/cloud-init.test.cjs.map +1 -0
- package/dist/generators/cloud-init.test.d.cts +1 -0
- package/dist/generators/cloud-init.test.d.mts +1 -0
- package/dist/generators/cloud-init.test.mjs +59 -0
- package/dist/generators/cloud-init.test.mjs.map +1 -0
- package/dist/generators/get-shit-done.test.cjs +48 -0
- package/dist/generators/get-shit-done.test.cjs.map +1 -0
- package/dist/generators/get-shit-done.test.d.cts +1 -0
- package/dist/generators/get-shit-done.test.d.mts +1 -0
- package/dist/generators/get-shit-done.test.mjs +49 -0
- package/dist/generators/get-shit-done.test.mjs.map +1 -0
- package/dist/generators/grafana.test.cjs +74 -0
- package/dist/generators/grafana.test.cjs.map +1 -0
- package/dist/generators/grafana.test.d.cts +1 -0
- package/dist/generators/grafana.test.d.mts +1 -0
- package/dist/generators/grafana.test.mjs +74 -0
- package/dist/generators/grafana.test.mjs.map +1 -0
- package/dist/generators/n8n-workflows.test.cjs +75 -0
- package/dist/generators/n8n-workflows.test.cjs.map +1 -0
- package/dist/generators/n8n-workflows.test.d.cts +1 -0
- package/dist/generators/n8n-workflows.test.d.mts +1 -0
- package/dist/generators/n8n-workflows.test.mjs +76 -0
- package/dist/generators/n8n-workflows.test.mjs.map +1 -0
- package/dist/generators/openclaw-install-script.test.cjs +35 -0
- package/dist/generators/openclaw-install-script.test.cjs.map +1 -0
- package/dist/generators/openclaw-install-script.test.d.cts +1 -0
- package/dist/generators/openclaw-install-script.test.d.mts +1 -0
- package/dist/generators/openclaw-install-script.test.mjs +36 -0
- package/dist/generators/openclaw-install-script.test.mjs.map +1 -0
- package/dist/generators/postgres-init.test.cjs +111 -0
- package/dist/generators/postgres-init.test.cjs.map +1 -0
- package/dist/generators/postgres-init.test.d.cts +1 -0
- package/dist/generators/postgres-init.test.d.mts +1 -0
- package/dist/generators/postgres-init.test.mjs +112 -0
- package/dist/generators/postgres-init.test.mjs.map +1 -0
- package/dist/generators/prometheus.test.cjs +99 -0
- package/dist/generators/prometheus.test.cjs.map +1 -0
- package/dist/generators/prometheus.test.d.cts +1 -0
- package/dist/generators/prometheus.test.d.mts +1 -0
- package/dist/generators/prometheus.test.mjs +99 -0
- package/dist/generators/prometheus.test.mjs.map +1 -0
- package/dist/generators/stack-manifest.test.cjs +97 -0
- package/dist/generators/stack-manifest.test.cjs.map +1 -0
- package/dist/generators/stack-manifest.test.d.cts +1 -0
- package/dist/generators/stack-manifest.test.d.mts +1 -0
- package/dist/generators/stack-manifest.test.mjs +98 -0
- package/dist/generators/stack-manifest.test.mjs.map +1 -0
- package/dist/index.cjs +0 -2
- package/dist/index.d.cts +1 -2
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +1 -2
- package/dist/logger/index.cjs +0 -2
- package/dist/logger/index.d.cts +1 -2
- package/dist/logger/index.d.mts +1 -2
- package/dist/logger/index.mjs +1 -2
- package/dist/port-scanner.test.cjs +155 -0
- package/dist/port-scanner.test.cjs.map +1 -0
- package/dist/port-scanner.test.d.cts +1 -0
- package/dist/port-scanner.test.d.mts +1 -0
- package/dist/port-scanner.test.mjs +156 -0
- package/dist/port-scanner.test.mjs.map +1 -0
- package/package.json +1 -1
- package/src/deployers/coolify.test.ts +180 -0
- package/src/deployers/dokploy.test.ts +120 -0
- package/src/frameworks/frameworks.test.ts +119 -0
- package/src/generators/cloud-init.test.ts +70 -0
- package/src/generators/get-shit-done.test.ts +54 -0
- package/src/generators/grafana.test.ts +90 -0
- package/src/generators/n8n-workflows.test.ts +80 -0
- package/src/generators/openclaw-install-script.test.ts +42 -0
- package/src/generators/postgres-init.test.ts +116 -0
- package/src/generators/prometheus.test.ts +108 -0
- package/src/generators/stack-manifest.test.ts +104 -0
- package/src/index.ts +3 -2
- package/src/logger/index.ts +2 -1
- package/src/port-scanner.test.ts +167 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { GenerationInput, ResolverOutput } from "../types.js";
|
|
3
|
+
import { generateStackManifest, type StackManifest } from "./stack-manifest.js";
|
|
4
|
+
|
|
5
|
+
function makeResolved(serviceIds: string[]): ResolverOutput {
|
|
6
|
+
return {
|
|
7
|
+
services: serviceIds.map((id) => ({
|
|
8
|
+
definition: {
|
|
9
|
+
id,
|
|
10
|
+
name: id.charAt(0).toUpperCase() + id.slice(1),
|
|
11
|
+
description: `${id} service`,
|
|
12
|
+
icon: "📦",
|
|
13
|
+
category: "test",
|
|
14
|
+
image: `${id}:latest`,
|
|
15
|
+
imageTag: "latest",
|
|
16
|
+
ports: [{ container: 8080, host: 8080, exposed: true, description: `${id} port` }],
|
|
17
|
+
volumes: [],
|
|
18
|
+
environment: [],
|
|
19
|
+
dependencies: [],
|
|
20
|
+
conflicts: [],
|
|
21
|
+
skills: [],
|
|
22
|
+
memoryMB: 256,
|
|
23
|
+
docsUrl: `https://docs.example.com/${id}`,
|
|
24
|
+
},
|
|
25
|
+
addedBy: "user" as const,
|
|
26
|
+
})),
|
|
27
|
+
addedDependencies: [],
|
|
28
|
+
estimatedMemoryMB: 512,
|
|
29
|
+
} as unknown as ResolverOutput;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const baseInput: GenerationInput = {
|
|
33
|
+
projectName: "test-stack",
|
|
34
|
+
services: ["redis"],
|
|
35
|
+
skillPacks: [],
|
|
36
|
+
proxy: "caddy",
|
|
37
|
+
domain: "example.com",
|
|
38
|
+
gpu: false,
|
|
39
|
+
platform: "linux/amd64",
|
|
40
|
+
deployment: "local",
|
|
41
|
+
generateSecrets: true,
|
|
42
|
+
openclawVersion: "latest",
|
|
43
|
+
} as unknown as GenerationInput;
|
|
44
|
+
|
|
45
|
+
describe("generateStackManifest", () => {
|
|
46
|
+
it("returns a single file: stack-manifest.json", () => {
|
|
47
|
+
const files = generateStackManifest(makeResolved(["redis"]), baseInput);
|
|
48
|
+
expect(Object.keys(files)).toEqual(["stack-manifest.json"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("produces valid JSON", () => {
|
|
52
|
+
const files = generateStackManifest(makeResolved(["redis"]), baseInput);
|
|
53
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
54
|
+
expect(manifest).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("includes format version", () => {
|
|
58
|
+
const files = generateStackManifest(makeResolved(["redis"]), baseInput);
|
|
59
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
60
|
+
expect(manifest.formatVersion).toBe("1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("includes project name from input", () => {
|
|
64
|
+
const files = generateStackManifest(makeResolved(["redis"]), baseInput);
|
|
65
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
66
|
+
expect(manifest.projectName).toBe("test-stack");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes deployment configuration", () => {
|
|
70
|
+
const files = generateStackManifest(makeResolved(["redis"]), baseInput);
|
|
71
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
72
|
+
expect(manifest.deployment).toBe("local");
|
|
73
|
+
expect(manifest.proxy).toBe("caddy");
|
|
74
|
+
expect(manifest.domain).toBe("example.com");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("lists all services with correct structure", () => {
|
|
78
|
+
const files = generateStackManifest(makeResolved(["redis", "postgresql"]), baseInput);
|
|
79
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
80
|
+
expect(manifest.services).toHaveLength(2);
|
|
81
|
+
for (const svc of manifest.services) {
|
|
82
|
+
expect(svc).toHaveProperty("id");
|
|
83
|
+
expect(svc).toHaveProperty("name");
|
|
84
|
+
expect(svc).toHaveProperty("category");
|
|
85
|
+
expect(svc).toHaveProperty("ports");
|
|
86
|
+
expect(svc).toHaveProperty("docsUrl");
|
|
87
|
+
expect(svc).toHaveProperty("addedBy");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("includes metadata with service count", () => {
|
|
92
|
+
const files = generateStackManifest(makeResolved(["redis", "postgresql"]), baseInput);
|
|
93
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
94
|
+
expect(manifest.metadata.serviceCount).toBe(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("includes generatedAt timestamp", () => {
|
|
98
|
+
const files = generateStackManifest(makeResolved(["redis"]), baseInput);
|
|
99
|
+
const manifest: StackManifest = JSON.parse(files["stack-manifest.json"]!);
|
|
100
|
+
expect(manifest.generatedAt).toBeDefined();
|
|
101
|
+
// Should be ISO 8601
|
|
102
|
+
expect(() => new Date(manifest.generatedAt)).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -81,14 +81,15 @@ export type {
|
|
|
81
81
|
OperationsLoggerOptions,
|
|
82
82
|
} from "./logger/index.js";
|
|
83
83
|
// ─── Operations Logger ─────────────────────────────────────────────────────
|
|
84
|
+
// Note: FileSink is excluded from the barrel export because it depends on
|
|
85
|
+
// Node.js `fs` and breaks browser/edge bundlers (e.g. Next.js webpack).
|
|
86
|
+
// Import it directly: import { FileSink } from "@better-openclaw/core/logger/sinks/file-sink"
|
|
84
87
|
export {
|
|
85
88
|
CallbackSink,
|
|
86
89
|
ConsoleSink,
|
|
87
|
-
FileSink,
|
|
88
90
|
OperationsLogger,
|
|
89
91
|
StepTracker,
|
|
90
92
|
} from "./logger/index.js";
|
|
91
|
-
export type { FileSinkOptions } from "./logger/sinks/file-sink.js";
|
|
92
93
|
// ─── Config Migrations ──────────────────────────────────────────────────────
|
|
93
94
|
export {
|
|
94
95
|
CURRENT_CONFIG_VERSION,
|
package/src/logger/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { OperationsLogger, StepTracker } from "./logger.js";
|
|
2
2
|
export { CallbackSink } from "./sinks/callback-sink.js";
|
|
3
3
|
export { ConsoleSink } from "./sinks/console-sink.js";
|
|
4
|
-
|
|
4
|
+
// FileSink is NOT re-exported here because it depends on Node.js `fs`.
|
|
5
|
+
// Import it directly: import { FileSink } from "@better-openclaw/core/logger/sinks/file-sink"
|
|
5
6
|
export type {
|
|
6
7
|
LogLevel,
|
|
7
8
|
LogSink,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { formatPortConflicts, scanPortConflicts } from "./port-scanner.js";
|
|
3
|
+
import type { ServiceDefinition } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for port scanner conflict detection and reassignment.
|
|
7
|
+
*
|
|
8
|
+
* We mock isPortAvailable via vi.mock to avoid actual port scanning
|
|
9
|
+
* during tests. The focus is on the conflict detection logic.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Mock the internal isPortAvailable to control port availability
|
|
13
|
+
vi.mock("node:net", () => {
|
|
14
|
+
const portsInUse = new Set([80, 443, 3000]);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
default: {
|
|
18
|
+
Socket: class MockSocket {
|
|
19
|
+
connect(port: number) {
|
|
20
|
+
// Simulate: ports in portsInUse are "in use"
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
if (portsInUse.has(port)) {
|
|
23
|
+
(this as any).connectCb?.();
|
|
24
|
+
} else {
|
|
25
|
+
(this as any).errorCb?.();
|
|
26
|
+
}
|
|
27
|
+
}, 1);
|
|
28
|
+
}
|
|
29
|
+
once(event: string, cb: () => void) {
|
|
30
|
+
if (event === "connect") (this as any).connectCb = cb;
|
|
31
|
+
if (event === "error") (this as any).errorCb = cb;
|
|
32
|
+
if (event === "timeout") (this as any).timeoutCb = cb;
|
|
33
|
+
}
|
|
34
|
+
setTimeout() {}
|
|
35
|
+
removeAllListeners() {}
|
|
36
|
+
destroy() {}
|
|
37
|
+
},
|
|
38
|
+
createServer() {
|
|
39
|
+
return {
|
|
40
|
+
once(event: string, cb: (err?: Error) => void) {
|
|
41
|
+
if (event === "error") (this as any).errorCb = cb;
|
|
42
|
+
},
|
|
43
|
+
listen(_port: number, _host: string, cb: () => void) {
|
|
44
|
+
cb();
|
|
45
|
+
},
|
|
46
|
+
close(cb: () => void) {
|
|
47
|
+
cb();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function makeService(
|
|
56
|
+
id: string,
|
|
57
|
+
ports: { host: number; container: number; exposed: boolean; description: string }[],
|
|
58
|
+
): ServiceDefinition {
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
name: id,
|
|
62
|
+
description: `${id} service`,
|
|
63
|
+
icon: "",
|
|
64
|
+
category: "test",
|
|
65
|
+
image: `${id}:latest`,
|
|
66
|
+
ports,
|
|
67
|
+
volumes: [],
|
|
68
|
+
environment: [],
|
|
69
|
+
dependencies: [],
|
|
70
|
+
conflicts: [],
|
|
71
|
+
skills: [],
|
|
72
|
+
memoryMB: 256,
|
|
73
|
+
docsUrl: "",
|
|
74
|
+
} as ServiceDefinition;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("formatPortConflicts", () => {
|
|
78
|
+
it("returns empty array when no reassignments", () => {
|
|
79
|
+
const services = [
|
|
80
|
+
makeService("redis", [{ host: 6379, container: 6379, exposed: true, description: "Redis" }]),
|
|
81
|
+
];
|
|
82
|
+
const reassignments = new Map();
|
|
83
|
+
const conflicts = formatPortConflicts(services, reassignments);
|
|
84
|
+
expect(conflicts).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns conflicts with suggested ports", () => {
|
|
88
|
+
const services = [
|
|
89
|
+
makeService("redis", [{ host: 6379, container: 6379, exposed: true, description: "Redis" }]),
|
|
90
|
+
];
|
|
91
|
+
const reassignments = new Map([["redis", new Map([[6379, 7379]])]]);
|
|
92
|
+
const conflicts = formatPortConflicts(services, reassignments);
|
|
93
|
+
expect(conflicts).toHaveLength(1);
|
|
94
|
+
expect(conflicts[0]).toEqual({
|
|
95
|
+
port: 6379,
|
|
96
|
+
serviceId: "redis",
|
|
97
|
+
description: "Redis",
|
|
98
|
+
suggestedPort: 7379,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("handles multiple services with multiple port reassignments", () => {
|
|
103
|
+
const services = [
|
|
104
|
+
makeService("web", [
|
|
105
|
+
{ host: 80, container: 80, exposed: true, description: "HTTP" },
|
|
106
|
+
{ host: 443, container: 443, exposed: true, description: "HTTPS" },
|
|
107
|
+
]),
|
|
108
|
+
makeService("api", [{ host: 8080, container: 8080, exposed: true, description: "API" }]),
|
|
109
|
+
];
|
|
110
|
+
const reassignments = new Map([
|
|
111
|
+
[
|
|
112
|
+
"web",
|
|
113
|
+
new Map([
|
|
114
|
+
[80, 1080],
|
|
115
|
+
[443, 1443],
|
|
116
|
+
]),
|
|
117
|
+
],
|
|
118
|
+
]);
|
|
119
|
+
const conflicts = formatPortConflicts(services, reassignments);
|
|
120
|
+
expect(conflicts).toHaveLength(2);
|
|
121
|
+
expect(conflicts[0]!.port).toBe(80);
|
|
122
|
+
expect(conflicts[0]!.suggestedPort).toBe(1080);
|
|
123
|
+
expect(conflicts[1]!.port).toBe(443);
|
|
124
|
+
expect(conflicts[1]!.suggestedPort).toBe(1443);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("uses service name as fallback when port has no description", () => {
|
|
128
|
+
const services = [
|
|
129
|
+
makeService("myapp", [{ host: 9000, container: 9000, exposed: true, description: "" }]),
|
|
130
|
+
];
|
|
131
|
+
const reassignments = new Map([["myapp", new Map([[9000, 10000]])]]);
|
|
132
|
+
const conflicts = formatPortConflicts(services, reassignments);
|
|
133
|
+
expect(conflicts[0]!.description).toBe("myapp port");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("scanPortConflicts", () => {
|
|
138
|
+
it("returns empty map for services with no ports", async () => {
|
|
139
|
+
const services = [makeService("redis", [])];
|
|
140
|
+
const result = await scanPortConflicts(services);
|
|
141
|
+
expect(result.size).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("returns empty map for services with no exposed ports", async () => {
|
|
145
|
+
const services = [
|
|
146
|
+
makeService("redis", [{ host: 6379, container: 6379, exposed: false, description: "Redis" }]),
|
|
147
|
+
];
|
|
148
|
+
const result = await scanPortConflicts(services);
|
|
149
|
+
expect(result.size).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("detects inter-service port conflicts", async () => {
|
|
153
|
+
// Two services claiming the same host port
|
|
154
|
+
const services = [
|
|
155
|
+
makeService("web1", [{ host: 8080, container: 80, exposed: true, description: "HTTP" }]),
|
|
156
|
+
makeService("web2", [{ host: 8080, container: 80, exposed: true, description: "HTTP" }]),
|
|
157
|
+
];
|
|
158
|
+
const result = await scanPortConflicts(services);
|
|
159
|
+
|
|
160
|
+
// First service wins, second gets reassigned
|
|
161
|
+
expect(result.has("web1")).toBe(false); // web1 keeps its port
|
|
162
|
+
expect(result.has("web2")).toBe(true); // web2 gets reassigned
|
|
163
|
+
const web2Reassignments = result.get("web2")!;
|
|
164
|
+
expect(web2Reassignments.has(8080)).toBe(true);
|
|
165
|
+
expect(web2Reassignments.get(8080)).toBeGreaterThan(8080);
|
|
166
|
+
});
|
|
167
|
+
});
|