@buildepicshit/cli 0.0.2 → 0.0.4
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/package.json +5 -3
- package/src/bes.ts +124 -0
- package/src/config-loader.ts +68 -0
- package/src/daemon.ts +127 -0
- package/src/orchestrator/core/__tests__/command.test.ts +264 -0
- package/src/orchestrator/core/__tests__/token.test.ts +145 -0
- package/src/orchestrator/core/command.ts +334 -0
- package/src/orchestrator/core/compound.ts +84 -0
- package/src/orchestrator/core/health-check.ts +38 -0
- package/src/orchestrator/core/index.ts +39 -0
- package/src/orchestrator/core/token.ts +88 -0
- package/src/orchestrator/core/types.ts +102 -0
- package/src/orchestrator/index.ts +67 -0
- package/src/orchestrator/logger/logger.ts +123 -0
- package/src/orchestrator/presets/docker.ts +164 -0
- package/src/orchestrator/presets/drizzle.ts +93 -0
- package/src/orchestrator/presets/esbuild.ts +12 -0
- package/src/orchestrator/presets/hono.ts +7 -0
- package/src/orchestrator/presets/index.ts +6 -0
- package/src/orchestrator/presets/nextjs.ts +97 -0
- package/src/orchestrator/presets/node.ts +12 -0
- package/src/orchestrator/runner/__tests__/event-bus.test.ts +97 -0
- package/src/orchestrator/runner/event-bus.ts +55 -0
- package/src/orchestrator/runner/health-runner.ts +129 -0
- package/src/orchestrator/runner/index.ts +17 -0
- package/src/orchestrator/runner/process.ts +167 -0
- package/src/orchestrator/runner/runtime-context.ts +51 -0
- package/src/orchestrator/utils/__tests__/dna.test.ts +133 -0
- package/src/orchestrator/utils/dna.ts +85 -0
- package/src/project.ts +40 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ComputeContext } from "../../core/types.js";
|
|
3
|
+
import { identity } from "../dna.js";
|
|
4
|
+
|
|
5
|
+
const ctx: ComputeContext = {
|
|
6
|
+
cwd: "/projects/myapp",
|
|
7
|
+
env: {},
|
|
8
|
+
root: "/projects/myapp",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function resolveToken(
|
|
12
|
+
token: ReturnType<ReturnType<typeof identity>["port"]>,
|
|
13
|
+
context: ComputeContext,
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
return typeof token === "function" ? token(context) : token;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("DNA", () => {
|
|
19
|
+
it("port() returns a number in range 3000–9999", async () => {
|
|
20
|
+
const result = await resolveToken(identity("hono").port(), ctx);
|
|
21
|
+
const port = Number.parseInt(result, 10);
|
|
22
|
+
expect(port).toBeGreaterThanOrEqual(3000);
|
|
23
|
+
expect(port).toBeLessThanOrEqual(9999);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("port() is deterministic for same cwd + suffix", async () => {
|
|
27
|
+
const r1 = await resolveToken(identity("hono").port(), ctx);
|
|
28
|
+
const r2 = await resolveToken(identity("hono").port(), ctx);
|
|
29
|
+
expect(r1).toBe(r2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("port() differs for different suffixes", async () => {
|
|
33
|
+
const r1 = await resolveToken(identity("hono").port(), ctx);
|
|
34
|
+
const r2 = await resolveToken(identity("next").port(), ctx);
|
|
35
|
+
expect(r1).not.toBe(r2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("port() differs for different cwds", async () => {
|
|
39
|
+
const ctx2: ComputeContext = { cwd: "/other/project", env: {}, root: "/" };
|
|
40
|
+
const r1 = await resolveToken(identity("hono").port(), ctx);
|
|
41
|
+
const r2 = await resolveToken(identity("hono").port(), ctx2);
|
|
42
|
+
expect(r1).not.toBe(r2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("url() returns http://localhost:{port}", async () => {
|
|
46
|
+
const result = await resolveToken(identity("hono").url(), ctx);
|
|
47
|
+
expect(result).toMatch(/^http:\/\/localhost:\d+$/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("url(path) appends the path", async () => {
|
|
51
|
+
const result = await resolveToken(identity("hono").url("/health"), ctx);
|
|
52
|
+
expect(result).toMatch(/^http:\/\/localhost:\d+\/health$/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("url() port matches port()", async () => {
|
|
56
|
+
const port = await resolveToken(identity("hono").port(), ctx);
|
|
57
|
+
const url = await resolveToken(identity("hono").url("/api"), ctx);
|
|
58
|
+
expect(url).toBe(`http://localhost:${port}/api`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("has a name property matching the suffix", () => {
|
|
62
|
+
expect(identity("server").name).toBe("server");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("has an empty name when no suffix is provided", () => {
|
|
66
|
+
expect(identity().name).toBe("");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("localhostUrl() returns http://localhost:{port}", async () => {
|
|
70
|
+
const result = await resolveToken(identity("hono").localhostUrl(), ctx);
|
|
71
|
+
expect(result).toMatch(/^http:\/\/localhost:\d+$/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("localhostUrl(path) appends the path", async () => {
|
|
75
|
+
const result = await resolveToken(
|
|
76
|
+
identity("hono").localhostUrl("/health"),
|
|
77
|
+
ctx,
|
|
78
|
+
);
|
|
79
|
+
expect(result).toMatch(/^http:\/\/localhost:\d+\/health$/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("localhostUrl() matches url() output", async () => {
|
|
83
|
+
const id = identity("hono");
|
|
84
|
+
const fromUrl = await resolveToken(id.url("/api"), ctx);
|
|
85
|
+
const fromLocalhostUrl = await resolveToken(id.localhostUrl("/api"), ctx);
|
|
86
|
+
expect(fromLocalhostUrl).toBe(fromUrl);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("port(identityName) computes port for a different identity", async () => {
|
|
90
|
+
const id = identity("infra");
|
|
91
|
+
const ownPort = await resolveToken(id.port(), ctx);
|
|
92
|
+
const dbPort = await resolveToken(id.port("database"), ctx);
|
|
93
|
+
expect(ownPort).not.toBe(dbPort);
|
|
94
|
+
const port = Number.parseInt(dbPort, 10);
|
|
95
|
+
expect(port).toBeGreaterThanOrEqual(3000);
|
|
96
|
+
expect(port).toBeLessThanOrEqual(9999);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("port(identityName) is deterministic", async () => {
|
|
100
|
+
const id = identity("infra");
|
|
101
|
+
const r1 = await resolveToken(id.port("database"), ctx);
|
|
102
|
+
const r2 = await resolveToken(id.port("database"), ctx);
|
|
103
|
+
expect(r1).toBe(r2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("port(identityName) matches standalone identity port", async () => {
|
|
107
|
+
const infra = identity("infra");
|
|
108
|
+
const db = identity("database");
|
|
109
|
+
const fromSub = await resolveToken(infra.port("database"), ctx);
|
|
110
|
+
const fromStandalone = await resolveToken(db.port(), ctx);
|
|
111
|
+
expect(fromSub).toBe(fromStandalone);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("url with portIdentity option uses different port", async () => {
|
|
115
|
+
const id = identity("infra");
|
|
116
|
+
const minioPort = await resolveToken(id.port("minio"), ctx);
|
|
117
|
+
const url = await resolveToken(
|
|
118
|
+
id.url("/health", { portIdentity: "minio" }),
|
|
119
|
+
ctx,
|
|
120
|
+
);
|
|
121
|
+
expect(url).toBe(`http://localhost:${minioPort}/health`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("localhostUrl with portIdentity option uses different port", async () => {
|
|
125
|
+
const id = identity("infra");
|
|
126
|
+
const minioPort = await resolveToken(id.port("minio"), ctx);
|
|
127
|
+
const url = await resolveToken(
|
|
128
|
+
id.localhostUrl("/health", { portIdentity: "minio" }),
|
|
129
|
+
ctx,
|
|
130
|
+
);
|
|
131
|
+
expect(url).toBe(`http://localhost:${minioPort}/health`);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Token } from "../core/types.js";
|
|
3
|
+
|
|
4
|
+
// Well-known/reserved ports in the 3000-9999 range to avoid
|
|
5
|
+
const RESERVED_PORTS = new Set([
|
|
6
|
+
3306, // MySQL
|
|
7
|
+
3389, // RDP
|
|
8
|
+
4369, // Erlang EPMD
|
|
9
|
+
5432, // PostgreSQL
|
|
10
|
+
5433, // PostgreSQL alt
|
|
11
|
+
5672, // AMQP (RabbitMQ)
|
|
12
|
+
5984, // CouchDB
|
|
13
|
+
6379, // Redis
|
|
14
|
+
6380, // Redis alt
|
|
15
|
+
6660, 6661, 6662, 6663, 6664, 6665, 6666, 6667, 6668, 6669, // IRC
|
|
16
|
+
6697, // IRC over TLS (ircs-u)
|
|
17
|
+
7474, // Neo4j
|
|
18
|
+
8080, // HTTP alternate
|
|
19
|
+
8443, // HTTPS alternate
|
|
20
|
+
8888, // Common dev port
|
|
21
|
+
9042, // Cassandra
|
|
22
|
+
9090, // Prometheus
|
|
23
|
+
9200, // Elasticsearch HTTP
|
|
24
|
+
9300, // Elasticsearch transport
|
|
25
|
+
9418, // Git
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function computePort(
|
|
29
|
+
cwd: string,
|
|
30
|
+
suffix: string,
|
|
31
|
+
range = { max: 9999, min: 3000 },
|
|
32
|
+
): number {
|
|
33
|
+
const span = range.max - range.min + 1;
|
|
34
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
35
|
+
const input = attempt === 0 ? `${cwd}:${suffix}` : `${cwd}:${suffix}:${attempt}`;
|
|
36
|
+
const hash = createHash("md5").update(input).digest("hex");
|
|
37
|
+
const num = Number.parseInt(hash.slice(0, 8), 16);
|
|
38
|
+
const port = range.min + (num % span);
|
|
39
|
+
if (!RESERVED_PORTS.has(port)) return port;
|
|
40
|
+
}
|
|
41
|
+
// Extremely unlikely fallback — just offset from the last attempt
|
|
42
|
+
const hash = createHash("md5").update(`${cwd}:${suffix}:fallback`).digest("hex");
|
|
43
|
+
const num = Number.parseInt(hash.slice(0, 8), 16);
|
|
44
|
+
return range.min + (num % span);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Identity {
|
|
48
|
+
localhostUrl: (
|
|
49
|
+
path?: string,
|
|
50
|
+
options?: { portIdentity?: string },
|
|
51
|
+
) => Token;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
port: (identityName?: string) => Token;
|
|
54
|
+
url: (path?: string, options?: { portIdentity?: string }) => Token;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createIdentity(suffix?: string): Identity {
|
|
58
|
+
const name = suffix || "";
|
|
59
|
+
|
|
60
|
+
const urlToken = (
|
|
61
|
+
pathValue?: string,
|
|
62
|
+
options?: { portIdentity?: string },
|
|
63
|
+
): Token => {
|
|
64
|
+
return async ({ cwd }) => {
|
|
65
|
+
const portName = options?.portIdentity ?? name;
|
|
66
|
+
const port = computePort(cwd, portName);
|
|
67
|
+
const base = `http://localhost:${port}`;
|
|
68
|
+
return pathValue ? `${base}${pathValue}` : base;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
localhostUrl: urlToken,
|
|
74
|
+
name,
|
|
75
|
+
port: (identityName?: string): Token => {
|
|
76
|
+
return async ({ cwd }) => {
|
|
77
|
+
const portName = identityName ?? name;
|
|
78
|
+
return computePort(cwd, portName).toString();
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
url: urlToken,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const identity = createIdentity;
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface ProjectMeta {
|
|
6
|
+
uid: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function besDir(cwd: string): string {
|
|
10
|
+
return join(cwd, ".bes");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function projectMetaPath(cwd: string): string {
|
|
14
|
+
return join(besDir(cwd), "project.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readOrCreateProjectMeta(cwd: string): ProjectMeta {
|
|
18
|
+
const path = projectMetaPath(cwd);
|
|
19
|
+
if (existsSync(path)) {
|
|
20
|
+
return JSON.parse(readFileSync(path, "utf-8")) as ProjectMeta;
|
|
21
|
+
}
|
|
22
|
+
const meta: ProjectMeta = { uid: randomUUID() };
|
|
23
|
+
const dir = besDir(cwd);
|
|
24
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
25
|
+
writeFileSync(path, JSON.stringify(meta, null, "\t"));
|
|
26
|
+
return meta;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readProjectName(cwd: string): string {
|
|
30
|
+
const pkgPath = join(cwd, "package.json");
|
|
31
|
+
if (existsSync(pkgPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
34
|
+
name?: string;
|
|
35
|
+
};
|
|
36
|
+
if (pkg.name) return pkg.name;
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
return basename(cwd);
|
|
40
|
+
}
|