@buildepicshit/cli 0.0.5 → 0.0.6
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 +1 -1
- package/src/orchestrator/core/__tests__/command.test.ts +69 -5
- package/src/orchestrator/core/command.ts +46 -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 +6 -4
- 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
|
@@ -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) {
|
|
@@ -215,17 +239,19 @@ class Command implements ICommand {
|
|
|
215
239
|
});
|
|
216
240
|
|
|
217
241
|
proc.onOutput((line, stream) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
242
|
+
if (!this.state.logsSilent) {
|
|
243
|
+
rtx.logger.output(name, line, stream);
|
|
244
|
+
rtx.eventBus.emit({
|
|
245
|
+
data: {
|
|
246
|
+
id: name,
|
|
247
|
+
line,
|
|
248
|
+
serviceName: name,
|
|
249
|
+
stream,
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
},
|
|
252
|
+
type: "log",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
229
255
|
});
|
|
230
256
|
|
|
231
257
|
if (this.state.healthChecks.length > 0) {
|
|
@@ -319,5 +345,6 @@ export function cmd(base: string): ICommand {
|
|
|
319
345
|
flags: [],
|
|
320
346
|
healthChecks: [],
|
|
321
347
|
identitySuffix: null,
|
|
348
|
+
logsSilent: false,
|
|
322
349
|
});
|
|
323
350
|
}
|
|
@@ -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[] {
|
|
@@ -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;
|