@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/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.5"
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 (dbPortToken as (ctx: ComputeContext) => Promise<string>)(ctx);
189
- const infraPort = await (infraPortToken as (ctx: ComputeContext) => Promise<string>)(ctx);
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", { portIdentity: "minio" });
198
- const url = await (urlToken as (ctx: ComputeContext) => Promise<string>)(ctx);
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 (minioPortToken as (ctx: ComputeContext) => Promise<string>)(ctx);
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
- return createIdentity(
45
- this.state.identitySuffix ?? this.deriveName(),
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
- rtx.logger.output(name, line, stream);
219
- rtx.eventBus.emit({
220
- data: {
221
- id: name,
222
- line,
223
- serviceName: name,
224
- stream,
225
- timestamp: Date.now(),
226
- },
227
- type: "log",
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
  }
@@ -29,7 +29,6 @@ export type {
29
29
  HealthCheckCallback,
30
30
  ICommand,
31
31
  ICompound,
32
-
33
32
  LazyEnvSource,
34
33
  ProcessStatus,
35
34
  Runnable,
@@ -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 { IDockerCompose, IDrizzle, INextjs, NextPreset } from "./presets/index.js";
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.flag("detach");
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.flag("detach");
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.flag("detach");
147
+ command = command.arg("-d");
146
148
  }
147
149
 
148
150
  if (options?.service) {
@@ -1,5 +1,5 @@
1
- export { type IDockerCompose, docker, dockerCompose } from "./docker.js";
2
- export { type IDrizzle, drizzle } from "./drizzle.js";
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, 6661, 6662, 6663, 6664, 6665, 6666, 6667, 6668, 6669, // IRC
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 = attempt === 0 ? `${cwd}:${suffix}` : `${cwd}:${suffix}:${attempt}`;
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").update(`${cwd}:${suffix}:fallback`).digest("hex");
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;