@buildepicshit/cli 0.0.10 → 0.0.12
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 +68 -0
- package/package.json +1 -1
- package/src/bes.typetest.ts +87 -0
- package/src/orchestrator/core/__tests__/command.test.ts +8 -7
- package/src/orchestrator/core/command.ts +55 -39
- package/src/orchestrator/core/index.ts +1 -0
- package/src/orchestrator/core/types.ts +5 -1
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/presets/docker.ts +3 -2
- package/src/orchestrator/presets/nextjs.ts +2 -1
- package/src/orchestrator/utils/port.ts +1 -3
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @buildepicshit/cli
|
|
2
|
+
|
|
3
|
+
The core orchestrator library and CLI binary for [Build Epic Shit](../../README.md).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @buildepicshit/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the [root README](../../README.md) for full API documentation, examples, and usage guide.
|
|
12
|
+
|
|
13
|
+
## What's in this package
|
|
14
|
+
|
|
15
|
+
- **Orchestrator API** — `cmd()`, `par()`, `seq()`, health checks, tokens, identity system
|
|
16
|
+
- **Framework presets** — `nextjs()`, `docker()`, `esbuild()`, `hono()`, `tsx()`, etc.
|
|
17
|
+
- **`bes` CLI binary** — reads `bes.config.ts` and turns exported functions into CLI commands
|
|
18
|
+
|
|
19
|
+
## Exports
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Main entry point
|
|
23
|
+
import { cmd, par, seq, health, fromEnv, fromFile, compute } from "@buildepicshit/cli"
|
|
24
|
+
|
|
25
|
+
// Presets
|
|
26
|
+
import { nextjs, dockerCompose, esbuild, tsx, hono, drizzle } from "@buildepicshit/cli/presets"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Development
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm dev # Watch mode (esbuild)
|
|
33
|
+
pnpm build # Production build
|
|
34
|
+
pnpm test # Run tests
|
|
35
|
+
pnpm typecheck # Type checking
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/
|
|
42
|
+
├── bes.ts # CLI entry point (commander)
|
|
43
|
+
├── config-loader.ts # bes.config.ts discovery and loading (jiti)
|
|
44
|
+
├── completion.ts # Shell tab-completion (omelette)
|
|
45
|
+
├── orchestrator/
|
|
46
|
+
│ ├── index.ts # Public API surface
|
|
47
|
+
│ ├── core/
|
|
48
|
+
│ │ ├── command.ts # ICommand implementation (immutable builder)
|
|
49
|
+
│ │ ├── compound.ts # par/seq implementation
|
|
50
|
+
│ │ ├── health-check.ts # Health check factories
|
|
51
|
+
│ │ ├── token.ts # Token resolution (compute, fromEnv, fromFile)
|
|
52
|
+
│ │ └── types.ts # All TypeScript interfaces
|
|
53
|
+
│ ├── presets/
|
|
54
|
+
│ │ ├── nextjs.ts # Next.js preset
|
|
55
|
+
│ │ ├── docker.ts # Docker/docker-compose preset
|
|
56
|
+
│ │ ├── drizzle.ts # Drizzle ORM preset
|
|
57
|
+
│ │ ├── esbuild.ts # esbuild preset
|
|
58
|
+
│ │ ├── hono.ts # Hono preset
|
|
59
|
+
│ │ └── node.ts # tsx/nodemon presets
|
|
60
|
+
│ ├── runner/
|
|
61
|
+
│ │ ├── event-bus.ts # Event emission for process lifecycle
|
|
62
|
+
│ │ ├── process.ts # Child process spawning
|
|
63
|
+
│ │ └── runtime-context.ts
|
|
64
|
+
│ ├── logger/
|
|
65
|
+
│ │ └── logger.ts # Structured logging
|
|
66
|
+
│ └── utils/
|
|
67
|
+
│ └── dna.ts # Identity/port allocation (deterministic hashing)
|
|
68
|
+
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is a typescript type test for the bes.config.ts file.
|
|
3
|
+
* It is used to test the types of the bes.config.ts file.
|
|
4
|
+
* It is not used in the production code.
|
|
5
|
+
* It is used to test the types of the bes.config.ts file.
|
|
6
|
+
* It is used to test the types of the bes.config.ts file.
|
|
7
|
+
* AI can't make changes to this file.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
cmd,
|
|
12
|
+
dockerCompose,
|
|
13
|
+
fromFile,
|
|
14
|
+
health,
|
|
15
|
+
nextjs,
|
|
16
|
+
par,
|
|
17
|
+
} from "@buildepicshit/cli";
|
|
18
|
+
|
|
19
|
+
const identities = {
|
|
20
|
+
database: "database",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const infra = dockerCompose()
|
|
24
|
+
.file("docker-compose.yml")
|
|
25
|
+
.up({ detach: true })
|
|
26
|
+
.withIdentity("database")
|
|
27
|
+
.env((self) => ({
|
|
28
|
+
DB_PORT: self.identity.port(identities.database),
|
|
29
|
+
}))
|
|
30
|
+
.logs({ silent: true })
|
|
31
|
+
.flag("docker-compose.yml")
|
|
32
|
+
.waitFor((self) => health.postgres(self.identity.port(identities.database)));
|
|
33
|
+
|
|
34
|
+
const api = cmd("tsx watch src/index.ts")
|
|
35
|
+
.withIdentity("api")
|
|
36
|
+
.env((self) => ({ PORT: self.identity.port(identities.database) }))
|
|
37
|
+
.dir("apps/api")
|
|
38
|
+
.waitFor((self) => health.http(self.identity.url("/livez")));
|
|
39
|
+
|
|
40
|
+
const envFile = fromFile(".env");
|
|
41
|
+
const dynamicApiEnv = {
|
|
42
|
+
DATABASE_URL: `postgresql://simly:simly@localhost:${infra.identity.port(identities.database)}/simly`,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const dynamicAppsEnv = {
|
|
46
|
+
API_URL: api.identity.localhostUrl(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const dbPush = cmd("drizzle-kit push")
|
|
50
|
+
.withIdentity("db-push")
|
|
51
|
+
.dir("apps/api")
|
|
52
|
+
.env(dynamicApiEnv)
|
|
53
|
+
.dependsOn(infra);
|
|
54
|
+
|
|
55
|
+
const dbStudio = cmd("drizzle-kit studio")
|
|
56
|
+
.withIdentity("db-studio")
|
|
57
|
+
.dir("apps/api")
|
|
58
|
+
.env(dynamicApiEnv)
|
|
59
|
+
.dependsOn(infra);
|
|
60
|
+
|
|
61
|
+
const playground = cmd("vite")
|
|
62
|
+
.withIdentity("playground")
|
|
63
|
+
.flag("port", (self) => self.identity.port())
|
|
64
|
+
.dependsOn(api)
|
|
65
|
+
.dir("apps/playground")
|
|
66
|
+
.env(envFile)
|
|
67
|
+
.env(dynamicAppsEnv);
|
|
68
|
+
|
|
69
|
+
const web = nextjs()
|
|
70
|
+
.withIdentity("web")
|
|
71
|
+
.env(envFile)
|
|
72
|
+
.flag("port", (self) => self.identity.port())
|
|
73
|
+
.env(dynamicAppsEnv)
|
|
74
|
+
.dependsOn(api)
|
|
75
|
+
.dir("apps/web")
|
|
76
|
+
.env(envFile);
|
|
77
|
+
|
|
78
|
+
export async function dev() {
|
|
79
|
+
await par(
|
|
80
|
+
infra,
|
|
81
|
+
dbPush,
|
|
82
|
+
dbStudio,
|
|
83
|
+
api.flag("watch"),
|
|
84
|
+
playground,
|
|
85
|
+
web.arg("dev"),
|
|
86
|
+
).run();
|
|
87
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { setConfigRoot } from "../../utils/config-context.js";
|
|
2
3
|
import { cmd } from "../command.js";
|
|
3
4
|
import { health } from "../health-check.js";
|
|
4
5
|
import { compute } from "../token.js";
|
|
5
6
|
import type { ComputeContext } from "../types.js";
|
|
6
|
-
import { setConfigRoot } from "../../utils/config-context.js";
|
|
7
7
|
|
|
8
8
|
const ctx: ComputeContext = {
|
|
9
9
|
cwd: "/app",
|
|
@@ -42,12 +42,13 @@ describe("cmd", () => {
|
|
|
42
42
|
expect(built.args).toEqual(["--port", "3000"]);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("resolves
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const built = await
|
|
50
|
-
|
|
45
|
+
it("resolves flag callback with self", async () => {
|
|
46
|
+
const c = cmd("next dev")
|
|
47
|
+
.withIdentity("web")
|
|
48
|
+
.flag("port", (self) => self.identity.port());
|
|
49
|
+
const built = await c.build(ctx);
|
|
50
|
+
const port = c.identity.port();
|
|
51
|
+
expect(built.args).toEqual(["--port", port]);
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
it("adds env vars via record", async () => {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type BuiltCommand,
|
|
13
13
|
type ComputeContext,
|
|
14
14
|
type EnvSource,
|
|
15
|
+
type FlagCallback,
|
|
15
16
|
type HealthCheck,
|
|
16
17
|
type HealthCheckCallback,
|
|
17
18
|
type ICommand,
|
|
@@ -29,7 +30,7 @@ interface CommandState {
|
|
|
29
30
|
readonly displayName: string | null;
|
|
30
31
|
readonly ensurePort: boolean;
|
|
31
32
|
readonly envSources: readonly EnvSource[];
|
|
32
|
-
readonly flags: readonly { name: string; value?:
|
|
33
|
+
readonly flags: readonly { name: string; value?: string }[];
|
|
33
34
|
readonly healthChecks: readonly HealthCheck[];
|
|
34
35
|
readonly identitySuffix: string | null;
|
|
35
36
|
readonly logsSilent: boolean;
|
|
@@ -82,10 +83,11 @@ class Command implements ICommand {
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
flag(name: string, value?:
|
|
86
|
+
flag(name: string, value?: string | FlagCallback): ICommand {
|
|
87
|
+
const resolved = typeof value === "function" ? value(this) : value;
|
|
86
88
|
return new Command({
|
|
87
89
|
...this.state,
|
|
88
|
-
flags: [...this.state.flags, { name, value }],
|
|
90
|
+
flags: [...this.state.flags, { name, value: resolved }],
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -141,8 +143,7 @@ class Command implements ICommand {
|
|
|
141
143
|
|
|
142
144
|
for (const flag of this.state.flags) {
|
|
143
145
|
if (flag.value !== undefined) {
|
|
144
|
-
|
|
145
|
-
args.push(`--${flag.name}`, resolved);
|
|
146
|
+
args.push(`--${flag.name}`, flag.value);
|
|
146
147
|
} else {
|
|
147
148
|
args.push(`--${flag.name}`);
|
|
148
149
|
}
|
|
@@ -199,28 +200,7 @@ class Command implements ICommand {
|
|
|
199
200
|
const built = await this.build(computeCtx);
|
|
200
201
|
|
|
201
202
|
if (rtx.dryRun) {
|
|
202
|
-
|
|
203
|
-
rtx.logger.system(` ${name}`);
|
|
204
|
-
rtx.logger.system(` cmd: ${fullCmd}`);
|
|
205
|
-
rtx.logger.system(` cwd: ${built.cwd}`);
|
|
206
|
-
const envKeys = Object.keys(built.env);
|
|
207
|
-
if (envKeys.length > 0) {
|
|
208
|
-
rtx.logger.system(` env: ${JSON.stringify(built.env)}`);
|
|
209
|
-
}
|
|
210
|
-
if (this.state.healthChecks.length > 0) {
|
|
211
|
-
const types = this.state.healthChecks.map((hc) => hc.type).join(", ");
|
|
212
|
-
rtx.logger.system(` waitFor: ${types}`);
|
|
213
|
-
}
|
|
214
|
-
if (this.state.dependencies.length > 0) {
|
|
215
|
-
const depNames = this.state.dependencies.flatMap((d) =>
|
|
216
|
-
d.collectNames(),
|
|
217
|
-
);
|
|
218
|
-
rtx.logger.system(` dependsOn: ${depNames.join(", ")}`);
|
|
219
|
-
}
|
|
220
|
-
if (this.state.ensurePort) {
|
|
221
|
-
rtx.logger.system(` ensurePort: ${this.identity.port()}`);
|
|
222
|
-
}
|
|
223
|
-
rtx.logger.system("");
|
|
203
|
+
this.logDryRun(name, built, rtx);
|
|
224
204
|
deferred.resolve();
|
|
225
205
|
return;
|
|
226
206
|
}
|
|
@@ -271,18 +251,7 @@ class Command implements ICommand {
|
|
|
271
251
|
});
|
|
272
252
|
|
|
273
253
|
if (this.state.healthChecks.length > 0) {
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
await waitForHealthy(check, computeCtx, proc);
|
|
277
|
-
} catch (err) {
|
|
278
|
-
rtx.logger.output(
|
|
279
|
-
name,
|
|
280
|
-
err instanceof Error ? err.message : String(err),
|
|
281
|
-
"stderr",
|
|
282
|
-
);
|
|
283
|
-
throw err;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
254
|
+
await this.awaitHealthChecks(name, computeCtx, proc, rtx);
|
|
286
255
|
proc.setStatus("healthy");
|
|
287
256
|
rtx.logger.healthy(name);
|
|
288
257
|
} else {
|
|
@@ -299,6 +268,53 @@ class Command implements ICommand {
|
|
|
299
268
|
});
|
|
300
269
|
}
|
|
301
270
|
|
|
271
|
+
private logDryRun(
|
|
272
|
+
name: string,
|
|
273
|
+
built: BuiltCommand,
|
|
274
|
+
rtx: ReturnType<typeof getRuntimeContext>,
|
|
275
|
+
): void {
|
|
276
|
+
const fullCmd = [built.command, ...built.args].join(" ");
|
|
277
|
+
rtx.logger.system(` ${name}`);
|
|
278
|
+
rtx.logger.system(` cmd: ${fullCmd}`);
|
|
279
|
+
rtx.logger.system(` cwd: ${built.cwd}`);
|
|
280
|
+
const envKeys = Object.keys(built.env);
|
|
281
|
+
if (envKeys.length > 0) {
|
|
282
|
+
rtx.logger.system(` env: ${JSON.stringify(built.env)}`);
|
|
283
|
+
}
|
|
284
|
+
if (this.state.healthChecks.length > 0) {
|
|
285
|
+
const types = this.state.healthChecks.map((hc) => hc.type).join(", ");
|
|
286
|
+
rtx.logger.system(` waitFor: ${types}`);
|
|
287
|
+
}
|
|
288
|
+
if (this.state.dependencies.length > 0) {
|
|
289
|
+
const depNames = this.state.dependencies.flatMap((d) => d.collectNames());
|
|
290
|
+
rtx.logger.system(` dependsOn: ${depNames.join(", ")}`);
|
|
291
|
+
}
|
|
292
|
+
if (this.state.ensurePort) {
|
|
293
|
+
rtx.logger.system(` ensurePort: ${this.identity.port()}`);
|
|
294
|
+
}
|
|
295
|
+
rtx.logger.system("");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async awaitHealthChecks(
|
|
299
|
+
name: string,
|
|
300
|
+
ctx: ComputeContext,
|
|
301
|
+
proc: ReturnType<typeof spawnProcess>,
|
|
302
|
+
rtx: ReturnType<typeof getRuntimeContext>,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
for (const check of this.state.healthChecks) {
|
|
305
|
+
try {
|
|
306
|
+
await waitForHealthy(check, ctx, proc);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
rtx.logger.output(
|
|
309
|
+
name,
|
|
310
|
+
err instanceof Error ? err.message : String(err),
|
|
311
|
+
"stderr",
|
|
312
|
+
);
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
302
318
|
private async waitForDependencies(): Promise<void> {
|
|
303
319
|
if (this.state.dependencies.length === 0) return;
|
|
304
320
|
|
|
@@ -10,6 +10,10 @@ export type ComputeContext = {
|
|
|
10
10
|
|
|
11
11
|
export type Token = string | ((ctx: ComputeContext) => Promise<string>);
|
|
12
12
|
|
|
13
|
+
// ─── Flag ────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export type FlagCallback = (self: ICommand) => string;
|
|
16
|
+
|
|
13
17
|
// ─── Env Source ──────────────────────────────────────────────────────────────
|
|
14
18
|
|
|
15
19
|
export interface LazyEnvSource {
|
|
@@ -56,7 +60,7 @@ export interface ICommand {
|
|
|
56
60
|
dir(path: string): ICommand;
|
|
57
61
|
ensurePort(): ICommand;
|
|
58
62
|
env(source: EnvSource): ICommand;
|
|
59
|
-
flag(name: string, value?:
|
|
63
|
+
flag(name: string, value?: string | FlagCallback): ICommand;
|
|
60
64
|
readonly identity: Identity;
|
|
61
65
|
logs(options: { silent?: boolean }): ICommand;
|
|
62
66
|
run(): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BuiltCommand,
|
|
4
4
|
ComputeContext,
|
|
5
5
|
EnvSource,
|
|
6
|
+
FlagCallback,
|
|
6
7
|
HealthCheck,
|
|
7
8
|
HealthCheckCallback,
|
|
8
9
|
ICommand,
|
|
@@ -67,7 +68,7 @@ class DockerCompose implements IDockerCompose {
|
|
|
67
68
|
return new DockerCompose(this.inner.logs(options), this.dcState);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
flag(name: string, value?:
|
|
71
|
+
flag(name: string, value?: string | FlagCallback): IDockerCompose {
|
|
71
72
|
return new DockerCompose(this.inner.flag(name, value), this.dcState);
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -136,7 +137,7 @@ export function dockerCompose(): IDockerCompose {
|
|
|
136
137
|
/** @deprecated Use `dockerCompose()` instead */
|
|
137
138
|
export function docker(options?: {
|
|
138
139
|
detach?: boolean;
|
|
139
|
-
file?:
|
|
140
|
+
file?: string;
|
|
140
141
|
service?: string;
|
|
141
142
|
}): ICommand {
|
|
142
143
|
let command = cmd("docker compose");
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BuiltCommand,
|
|
4
4
|
ComputeContext,
|
|
5
5
|
EnvSource,
|
|
6
|
+
FlagCallback,
|
|
6
7
|
HealthCheck,
|
|
7
8
|
HealthCheckCallback,
|
|
8
9
|
ICommand,
|
|
@@ -45,7 +46,7 @@ class Nextjs implements INextjs {
|
|
|
45
46
|
return new Nextjs(this.inner.logs(options));
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
flag(name: string, value?:
|
|
49
|
+
flag(name: string, value?: string | FlagCallback): INextjs {
|
|
49
50
|
return new Nextjs(this.inner.flag(name, value));
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -4,9 +4,7 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
* Find and kill any process listening on the given port.
|
|
5
5
|
* Returns the PID of the first process killed, or null if the port was free.
|
|
6
6
|
*/
|
|
7
|
-
export async function killProcessOnPort(
|
|
8
|
-
port: number,
|
|
9
|
-
): Promise<number | null> {
|
|
7
|
+
export async function killProcessOnPort(port: number): Promise<number | null> {
|
|
10
8
|
let pids: number[];
|
|
11
9
|
try {
|
|
12
10
|
const output = execSync(`lsof -ti :${port}`, {
|