@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 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
@@ -37,5 +37,5 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "type": "module",
40
- "version": "0.0.10"
40
+ "version": "0.0.12"
41
41
  }
@@ -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 compute tokens in flags", async () => {
46
- const token = compute(async ({ env }) =>
47
- env.NODE_ENV === "production" ? "8080" : "3000",
48
- );
49
- const built = await cmd("next dev").flag("port", token).build(ctx);
50
- expect(built.args).toEqual(["--port", "8080"]);
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?: Token }[];
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?: Token): ICommand {
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
- const resolved = await resolveToken(flag.value, ctx);
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
- const fullCmd = [built.command, ...built.args].join(" ");
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
- for (const check of this.state.healthChecks) {
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
 
@@ -25,6 +25,7 @@ export type {
25
25
  ComputeContext,
26
26
  EnvCallback,
27
27
  EnvSource,
28
+ FlagCallback,
28
29
  HealthCheck,
29
30
  HealthCheckCallback,
30
31
  ICommand,
@@ -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?: Token): ICommand;
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>;
@@ -21,6 +21,7 @@ export type {
21
21
  ComputeContext,
22
22
  EnvCallback,
23
23
  EnvSource,
24
+ FlagCallback,
24
25
  HealthCheck,
25
26
  HealthCheckCallback,
26
27
  ICommand,
@@ -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?: Token): IDockerCompose {
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?: Token;
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?: Token): INextjs {
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}`, {