@buildepicshit/cli 0.0.5 → 0.0.7
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/dist/bes.js +359 -0
- package/dist/bes.js.map +4 -4
- package/package.json +3 -1
- package/src/bes.ts +11 -0
- package/src/completion.ts +12 -0
- package/src/orchestrator/core/__tests__/command.test.ts +69 -5
- package/src/orchestrator/core/command.ts +48 -19
- package/src/orchestrator/core/index.ts +0 -1
- package/src/orchestrator/core/types.ts +1 -0
- package/src/orchestrator/index.ts +6 -2
- package/src/orchestrator/presets/docker.ts +9 -7
- package/src/orchestrator/presets/index.ts +2 -2
- package/src/orchestrator/presets/nextjs.ts +5 -0
- package/src/orchestrator/utils/dna.ts +16 -7
package/package.json
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
"commander": "^13.1.0",
|
|
8
8
|
"execa": "^9.6.1",
|
|
9
9
|
"jiti": "^2.4.2",
|
|
10
|
+
"omelette": "^0.4.17",
|
|
10
11
|
"open": "^10.1.2",
|
|
11
12
|
"ws": "^8.18.2",
|
|
12
13
|
"zx": "^8.8.5"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
16
|
+
"@types/omelette": "^0.4.5",
|
|
15
17
|
"@types/ws": "^8.18.1",
|
|
16
18
|
"esbuild": "^0.25.5"
|
|
17
19
|
},
|
|
@@ -35,5 +37,5 @@
|
|
|
35
37
|
"typecheck": "tsc --noEmit"
|
|
36
38
|
},
|
|
37
39
|
"type": "module",
|
|
38
|
-
"version": "0.0.
|
|
40
|
+
"version": "0.0.7"
|
|
39
41
|
}
|
package/src/bes.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { setupCompletion } from "./completion.js";
|
|
2
3
|
import { loadConfig } from "./config-loader.js";
|
|
3
4
|
import { createLazyLogger } from "./orchestrator/logger/logger.js";
|
|
4
5
|
import { createEventBus } from "./orchestrator/runner/event-bus.js";
|
|
@@ -18,6 +19,16 @@ program
|
|
|
18
19
|
|
|
19
20
|
async function main() {
|
|
20
21
|
const { root, commands } = await loadConfig();
|
|
22
|
+
const commandNames = Object.keys(commands);
|
|
23
|
+
|
|
24
|
+
// Setup shell autocomplete (handles completion requests and exits)
|
|
25
|
+
const completion = setupCompletion(commandNames);
|
|
26
|
+
|
|
27
|
+
// bes --setup-completion → install shell init file and exit
|
|
28
|
+
if (process.argv.includes("--setup-completion")) {
|
|
29
|
+
completion.setupShellInitFile();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
21
32
|
|
|
22
33
|
// Register each exported function as a CLI command
|
|
23
34
|
for (const [name, fn] of Object.entries(commands)) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import omelette from "omelette";
|
|
2
|
+
|
|
3
|
+
export function setupCompletion(commandNames: string[]) {
|
|
4
|
+
const completion = omelette("bes <command>");
|
|
5
|
+
|
|
6
|
+
completion.on("command", ({ reply }) => {
|
|
7
|
+
reply([...commandNames, "--dry"]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
completion.init();
|
|
11
|
+
return completion;
|
|
12
|
+
}
|
|
@@ -154,6 +154,32 @@ describe("command identity", () => {
|
|
|
154
154
|
expect(withId.identity.name).toBe("custom");
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
it("throws a helpful error when .identity is called as a function", () => {
|
|
158
|
+
const c = cmd("node server.js");
|
|
159
|
+
expect(() => {
|
|
160
|
+
(c.identity as unknown as (s: string) => void)("api");
|
|
161
|
+
}).toThrow(
|
|
162
|
+
'.identity("api") is not a function. To set identity, use .withIdentity("api") instead.',
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("throws a helpful error when .identity is called without args", () => {
|
|
167
|
+
const c = cmd("node server.js");
|
|
168
|
+
expect(() => {
|
|
169
|
+
(c.identity as unknown as () => void)();
|
|
170
|
+
}).toThrow(
|
|
171
|
+
".identity() is not a function. To set identity, use .withIdentity() instead.",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("identity proxy still exposes .port() and .url()", async () => {
|
|
176
|
+
const c = cmd("node server.js").withIdentity("api");
|
|
177
|
+
expect(c.identity.name).toBe("api");
|
|
178
|
+
expect(typeof c.identity.port()).toBe("function");
|
|
179
|
+
expect(typeof c.identity.url("/health")).toBe("function");
|
|
180
|
+
expect(typeof c.identity.localhostUrl()).toBe("function");
|
|
181
|
+
});
|
|
182
|
+
|
|
157
183
|
it("identity.port() returns a token", async () => {
|
|
158
184
|
const c = cmd("node server.js").withIdentity("api").dir("apps/api");
|
|
159
185
|
const token = c.identity.port();
|
|
@@ -185,8 +211,12 @@ describe("command identity", () => {
|
|
|
185
211
|
const c = cmd("docker compose").withIdentity("infra");
|
|
186
212
|
const dbPortToken = c.identity.port("database");
|
|
187
213
|
const infraPortToken = c.identity.port();
|
|
188
|
-
const dbPort = await (
|
|
189
|
-
|
|
214
|
+
const dbPort = await (
|
|
215
|
+
dbPortToken as (ctx: ComputeContext) => Promise<string>
|
|
216
|
+
)(ctx);
|
|
217
|
+
const infraPort = await (
|
|
218
|
+
infraPortToken as (ctx: ComputeContext) => Promise<string>
|
|
219
|
+
)(ctx);
|
|
190
220
|
// Named sub-identity should produce a different port
|
|
191
221
|
expect(dbPort).not.toBe(infraPort);
|
|
192
222
|
expect(Number.parseInt(dbPort, 10)).toBeGreaterThanOrEqual(3000);
|
|
@@ -194,13 +224,19 @@ describe("command identity", () => {
|
|
|
194
224
|
|
|
195
225
|
it("identity.localhostUrl with portIdentity option", async () => {
|
|
196
226
|
const c = cmd("docker compose").withIdentity("infra");
|
|
197
|
-
const urlToken = c.identity.localhostUrl("/health", {
|
|
198
|
-
|
|
227
|
+
const urlToken = c.identity.localhostUrl("/health", {
|
|
228
|
+
portIdentity: "minio",
|
|
229
|
+
});
|
|
230
|
+
const url = await (urlToken as (ctx: ComputeContext) => Promise<string>)(
|
|
231
|
+
ctx,
|
|
232
|
+
);
|
|
199
233
|
expect(url).toMatch(/^http:\/\/localhost:\d+\/health$/);
|
|
200
234
|
|
|
201
235
|
// Port in the URL should match minio identity, not infra
|
|
202
236
|
const minioPortToken = c.identity.port("minio");
|
|
203
|
-
const minioPort = await (
|
|
237
|
+
const minioPort = await (
|
|
238
|
+
minioPortToken as (ctx: ComputeContext) => Promise<string>
|
|
239
|
+
)(ctx);
|
|
204
240
|
expect(url).toBe(`http://localhost:${minioPort}/health`);
|
|
205
241
|
});
|
|
206
242
|
});
|
|
@@ -262,3 +298,31 @@ describe("env callback form", () => {
|
|
|
262
298
|
expect(Number.parseInt(built.env.PORT, 10)).toBeGreaterThanOrEqual(3000);
|
|
263
299
|
});
|
|
264
300
|
});
|
|
301
|
+
|
|
302
|
+
describe("logs", () => {
|
|
303
|
+
it("logs({ silent: true }) builds without error", async () => {
|
|
304
|
+
const c = cmd("node server.js").logs({ silent: true });
|
|
305
|
+
const built = await c.build(ctx);
|
|
306
|
+
expect(built).toBeDefined();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("logs state propagates through builder chain", async () => {
|
|
310
|
+
const c = cmd("node server.js")
|
|
311
|
+
.logs({ silent: true })
|
|
312
|
+
.dir("apps/api")
|
|
313
|
+
.env({ KEY: "value" })
|
|
314
|
+
.flag("verbose");
|
|
315
|
+
const built = await c.build(ctx);
|
|
316
|
+
expect(built).toBeDefined();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("is immutable — logs does not affect original", async () => {
|
|
320
|
+
const base = cmd("node server.js");
|
|
321
|
+
const silent = base.logs({ silent: true });
|
|
322
|
+
// Both should build fine — they are independent instances
|
|
323
|
+
const built1 = await base.build(ctx);
|
|
324
|
+
const built2 = await silent.build(ctx);
|
|
325
|
+
expect(built1).toBeDefined();
|
|
326
|
+
expect(built2).toBeDefined();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createDeferred,
|
|
6
6
|
getRuntimeContext,
|
|
7
7
|
} from "../runner/runtime-context.js";
|
|
8
|
-
import { createIdentity } from "../utils/dna.js";
|
|
8
|
+
import { createIdentity, type Identity } from "../utils/dna.js";
|
|
9
9
|
import { resolveToken } from "./token.js";
|
|
10
10
|
import {
|
|
11
11
|
type BuiltCommand,
|
|
@@ -30,6 +30,7 @@ interface CommandState {
|
|
|
30
30
|
readonly flags: readonly { name: string; value?: Token }[];
|
|
31
31
|
readonly healthChecks: readonly HealthCheck[];
|
|
32
32
|
readonly identitySuffix: string | null;
|
|
33
|
+
readonly logsSilent: boolean;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
class Command implements ICommand {
|
|
@@ -40,16 +41,41 @@ class Command implements ICommand {
|
|
|
40
41
|
this.state = state;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
get identity() {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
44
|
+
get identity(): Identity {
|
|
45
|
+
const id = createIdentity(this.state.identitySuffix ?? this.deriveName());
|
|
46
|
+
|
|
47
|
+
function throwOnCall(...args: unknown[]): never {
|
|
48
|
+
const hint = typeof args[0] === "string" ? `("${args[0]}")` : "()";
|
|
49
|
+
throw new TypeError(
|
|
50
|
+
`.identity${hint} is not a function. To set identity, use .withIdentity${hint} instead. ` +
|
|
51
|
+
`.identity is a read-only property that provides access to .port() and .url().`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Proxy(throwOnCall, {
|
|
56
|
+
apply(_target, _thisArg, args) {
|
|
57
|
+
throwOnCall(...(args as unknown[]));
|
|
58
|
+
},
|
|
59
|
+
get(_target, prop, receiver) {
|
|
60
|
+
return Reflect.get(id, prop, receiver);
|
|
61
|
+
},
|
|
62
|
+
has(_target, prop) {
|
|
63
|
+
return prop in id;
|
|
64
|
+
},
|
|
65
|
+
}) as unknown as Identity;
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
withIdentity(suffix: string): ICommand {
|
|
50
69
|
return new Command({ ...this.state, identitySuffix: suffix });
|
|
51
70
|
}
|
|
52
71
|
|
|
72
|
+
logs(options: { silent?: boolean }): ICommand {
|
|
73
|
+
return new Command({
|
|
74
|
+
...this.state,
|
|
75
|
+
logsSilent: options.silent ?? false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
flag(name: string, value?: Token): ICommand {
|
|
54
80
|
return new Command({
|
|
55
81
|
...this.state,
|
|
@@ -183,9 +209,7 @@ class Command implements ICommand {
|
|
|
183
209
|
rtx.logger.system(` env: ${JSON.stringify(built.env)}`);
|
|
184
210
|
}
|
|
185
211
|
if (this.state.healthChecks.length > 0) {
|
|
186
|
-
const types = this.state.healthChecks
|
|
187
|
-
.map((hc) => hc.type)
|
|
188
|
-
.join(", ");
|
|
212
|
+
const types = this.state.healthChecks.map((hc) => hc.type).join(", ");
|
|
189
213
|
rtx.logger.system(` waitFor: ${types}`);
|
|
190
214
|
}
|
|
191
215
|
if (this.state.dependencies.length > 0) {
|
|
@@ -199,7 +223,9 @@ class Command implements ICommand {
|
|
|
199
223
|
return;
|
|
200
224
|
}
|
|
201
225
|
|
|
226
|
+
const fullCmd = [built.command, ...built.args].join(" ");
|
|
202
227
|
rtx.logger.starting(name, name);
|
|
228
|
+
rtx.logger.system(` ${fullCmd}`);
|
|
203
229
|
|
|
204
230
|
const proc = spawnProcess(name, name, built);
|
|
205
231
|
rtx.processes.set(name, proc);
|
|
@@ -215,17 +241,19 @@ class Command implements ICommand {
|
|
|
215
241
|
});
|
|
216
242
|
|
|
217
243
|
proc.onOutput((line, stream) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
244
|
+
if (!this.state.logsSilent) {
|
|
245
|
+
rtx.logger.output(name, line, stream);
|
|
246
|
+
rtx.eventBus.emit({
|
|
247
|
+
data: {
|
|
248
|
+
id: name,
|
|
249
|
+
line,
|
|
250
|
+
serviceName: name,
|
|
251
|
+
stream,
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
},
|
|
254
|
+
type: "log",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
229
257
|
});
|
|
230
258
|
|
|
231
259
|
if (this.state.healthChecks.length > 0) {
|
|
@@ -319,5 +347,6 @@ export function cmd(base: string): ICommand {
|
|
|
319
347
|
flags: [],
|
|
320
348
|
healthChecks: [],
|
|
321
349
|
identitySuffix: null,
|
|
350
|
+
logsSilent: false,
|
|
322
351
|
});
|
|
323
352
|
}
|
|
@@ -57,6 +57,7 @@ export interface ICommand {
|
|
|
57
57
|
env(source: EnvSource): ICommand;
|
|
58
58
|
flag(name: string, value?: Token): ICommand;
|
|
59
59
|
readonly identity: Identity;
|
|
60
|
+
logs(options: { silent?: boolean }): ICommand;
|
|
60
61
|
run(): Promise<void>;
|
|
61
62
|
waitFor(check: HealthCheck | HealthCheckCallback): ICommand;
|
|
62
63
|
withIdentity(suffix: string): ICommand;
|
|
@@ -25,7 +25,6 @@ export type {
|
|
|
25
25
|
HealthCheckCallback,
|
|
26
26
|
ICommand,
|
|
27
27
|
ICompound,
|
|
28
|
-
|
|
29
28
|
ProcessStatus,
|
|
30
29
|
Runnable,
|
|
31
30
|
Token,
|
|
@@ -34,7 +33,12 @@ export type {
|
|
|
34
33
|
export type { Logger } from "./logger/logger.js";
|
|
35
34
|
export { createLazyLogger, createLogger } from "./logger/logger.js";
|
|
36
35
|
// Presets
|
|
37
|
-
export type {
|
|
36
|
+
export type {
|
|
37
|
+
IDockerCompose,
|
|
38
|
+
IDrizzle,
|
|
39
|
+
INextjs,
|
|
40
|
+
NextPreset,
|
|
41
|
+
} from "./presets/index.js";
|
|
38
42
|
export {
|
|
39
43
|
docker,
|
|
40
44
|
dockerCompose,
|
|
@@ -13,6 +13,7 @@ import type { Identity } from "../utils/dna.js";
|
|
|
13
13
|
|
|
14
14
|
export interface IDockerCompose extends ICommand {
|
|
15
15
|
file(path: string): IDockerCompose;
|
|
16
|
+
logs(options: { silent?: boolean }): IDockerCompose;
|
|
16
17
|
up(options?: { detach?: boolean }): IDockerCompose;
|
|
17
18
|
withIdentity(suffix: string): IDockerCompose;
|
|
18
19
|
}
|
|
@@ -58,6 +59,10 @@ class DockerCompose implements IDockerCompose {
|
|
|
58
59
|
return new DockerCompose(this.inner.withIdentity(suffix), this.dcState);
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
logs(options: { silent?: boolean }): IDockerCompose {
|
|
63
|
+
return new DockerCompose(this.inner.logs(options), this.dcState);
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
flag(name: string, value?: Token): IDockerCompose {
|
|
62
67
|
return new DockerCompose(this.inner.flag(name, value), this.dcState);
|
|
63
68
|
}
|
|
@@ -83,10 +88,7 @@ class DockerCompose implements IDockerCompose {
|
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
dependsOn(...deps: Runnable[]): IDockerCompose {
|
|
86
|
-
return new DockerCompose(
|
|
87
|
-
this.inner.dependsOn(...deps),
|
|
88
|
-
this.dcState,
|
|
89
|
-
);
|
|
91
|
+
return new DockerCompose(this.inner.dependsOn(...deps), this.dcState);
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
collectNames(): string[] {
|
|
@@ -101,7 +103,7 @@ class DockerCompose implements IDockerCompose {
|
|
|
101
103
|
}
|
|
102
104
|
command = command.arg("up");
|
|
103
105
|
if (this.dcState.detach) {
|
|
104
|
-
command = command.
|
|
106
|
+
command = command.arg("-d");
|
|
105
107
|
}
|
|
106
108
|
return command.build(ctx);
|
|
107
109
|
}
|
|
@@ -114,7 +116,7 @@ class DockerCompose implements IDockerCompose {
|
|
|
114
116
|
}
|
|
115
117
|
command = command.arg("up");
|
|
116
118
|
if (this.dcState.detach) {
|
|
117
|
-
command = command.
|
|
119
|
+
command = command.arg("-d");
|
|
118
120
|
}
|
|
119
121
|
return command.run();
|
|
120
122
|
}
|
|
@@ -142,7 +144,7 @@ export function docker(options?: {
|
|
|
142
144
|
command = command.arg("up");
|
|
143
145
|
|
|
144
146
|
if (options?.detach) {
|
|
145
|
-
command = command.
|
|
147
|
+
command = command.arg("-d");
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
if (options?.service) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { type IDrizzle
|
|
1
|
+
export { docker, dockerCompose, type IDockerCompose } from "./docker.js";
|
|
2
|
+
export { drizzle, type IDrizzle } from "./drizzle.js";
|
|
3
3
|
export { esbuild, esbuildWatch } from "./esbuild.js";
|
|
4
4
|
export { hono } from "./hono.js";
|
|
5
5
|
export { type INextjs, type NextPreset, next, nextjs } from "./nextjs.js";
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import type { Identity } from "../utils/dna.js";
|
|
13
13
|
|
|
14
14
|
export interface INextjs extends ICommand {
|
|
15
|
+
logs(options: { silent?: boolean }): INextjs;
|
|
15
16
|
withIdentity(suffix: string): INextjs;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -36,6 +37,10 @@ class Nextjs implements INextjs {
|
|
|
36
37
|
return new Nextjs(this.inner.withIdentity(suffix));
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
logs(options: { silent?: boolean }): INextjs {
|
|
41
|
+
return new Nextjs(this.inner.logs(options));
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
flag(name: string, value?: Token): INextjs {
|
|
40
45
|
return new Nextjs(this.inner.flag(name, value));
|
|
41
46
|
}
|
|
@@ -12,7 +12,16 @@ const RESERVED_PORTS = new Set([
|
|
|
12
12
|
5984, // CouchDB
|
|
13
13
|
6379, // Redis
|
|
14
14
|
6380, // Redis alt
|
|
15
|
-
6660,
|
|
15
|
+
6660,
|
|
16
|
+
6661,
|
|
17
|
+
6662,
|
|
18
|
+
6663,
|
|
19
|
+
6664,
|
|
20
|
+
6665,
|
|
21
|
+
6666,
|
|
22
|
+
6667,
|
|
23
|
+
6668,
|
|
24
|
+
6669, // IRC
|
|
16
25
|
6697, // IRC over TLS (ircs-u)
|
|
17
26
|
7474, // Neo4j
|
|
18
27
|
8080, // HTTP alternate
|
|
@@ -32,23 +41,23 @@ function computePort(
|
|
|
32
41
|
): number {
|
|
33
42
|
const span = range.max - range.min + 1;
|
|
34
43
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
35
|
-
const input =
|
|
44
|
+
const input =
|
|
45
|
+
attempt === 0 ? `${cwd}:${suffix}` : `${cwd}:${suffix}:${attempt}`;
|
|
36
46
|
const hash = createHash("md5").update(input).digest("hex");
|
|
37
47
|
const num = Number.parseInt(hash.slice(0, 8), 16);
|
|
38
48
|
const port = range.min + (num % span);
|
|
39
49
|
if (!RESERVED_PORTS.has(port)) return port;
|
|
40
50
|
}
|
|
41
51
|
// Extremely unlikely fallback — just offset from the last attempt
|
|
42
|
-
const hash = createHash("md5")
|
|
52
|
+
const hash = createHash("md5")
|
|
53
|
+
.update(`${cwd}:${suffix}:fallback`)
|
|
54
|
+
.digest("hex");
|
|
43
55
|
const num = Number.parseInt(hash.slice(0, 8), 16);
|
|
44
56
|
return range.min + (num % span);
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
export interface Identity {
|
|
48
|
-
localhostUrl: (
|
|
49
|
-
path?: string,
|
|
50
|
-
options?: { portIdentity?: string },
|
|
51
|
-
) => Token;
|
|
60
|
+
localhostUrl: (path?: string, options?: { portIdentity?: string }) => Token;
|
|
52
61
|
readonly name: string;
|
|
53
62
|
port: (identityName?: string) => Token;
|
|
54
63
|
url: (path?: string, options?: { portIdentity?: string }) => Token;
|