@buildepicshit/cli 0.0.4 → 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 CHANGED
@@ -35,5 +35,5 @@
35
35
  "typecheck": "tsc --noEmit"
36
36
  },
37
37
  "type": "module",
38
- "version": "0.0.4"
38
+ "version": "0.0.6"
39
39
  }
@@ -133,14 +133,14 @@ describe("command identity", () => {
133
133
  expect(c.identity.name).toBe("tsx");
134
134
  });
135
135
 
136
- it("overrides identity with .identity(suffix)", () => {
137
- const c = cmd("node server.js").identity("custom").dir("apps/api");
136
+ it("overrides identity with .withIdentity(suffix)", () => {
137
+ const c = cmd("node server.js").withIdentity("custom").dir("apps/api");
138
138
  expect(c.identity.name).toBe("custom");
139
139
  });
140
140
 
141
141
  it("identity suffix propagates through builder chain", () => {
142
142
  const c = cmd("node server.js")
143
- .identity("api")
143
+ .withIdentity("api")
144
144
  .dir("apps/server")
145
145
  .env({ KEY: "value" })
146
146
  .flag("verbose");
@@ -149,13 +149,39 @@ describe("command identity", () => {
149
149
 
150
150
  it("identity is immutable across branches", () => {
151
151
  const base = cmd("node server.js");
152
- const withId = base.identity("custom");
152
+ const withId = base.withIdentity("custom");
153
153
  expect(base.identity.name).toBe("node");
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
- const c = cmd("node server.js").identity("api").dir("apps/api");
184
+ const c = cmd("node server.js").withIdentity("api").dir("apps/api");
159
185
  const token = c.identity.port();
160
186
  expect(typeof token).toBe("function");
161
187
  const port = Number.parseInt(
@@ -167,14 +193,14 @@ describe("command identity", () => {
167
193
  });
168
194
 
169
195
  it("identity.localhostUrl() returns a token", async () => {
170
- const c = cmd("node server.js").identity("api");
196
+ const c = cmd("node server.js").withIdentity("api");
171
197
  const token = c.identity.localhostUrl("/health");
172
198
  const url = await (token as (ctx: ComputeContext) => Promise<string>)(ctx);
173
199
  expect(url).toMatch(/^http:\/\/localhost:\d+\/health$/);
174
200
  });
175
201
 
176
202
  it("cross-command identity access works", async () => {
177
- const server = cmd("node server.js").identity("api").dir("apps/api");
203
+ const server = cmd("node server.js").withIdentity("api").dir("apps/api");
178
204
  const built = await cmd("next dev")
179
205
  .env({ API_URL: server.identity.localhostUrl() })
180
206
  .build(ctx);
@@ -182,25 +208,35 @@ describe("command identity", () => {
182
208
  });
183
209
 
184
210
  it("identity.port(name) resolves a named sub-identity", async () => {
185
- const c = cmd("docker compose").identity("infra");
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);
193
223
  });
194
224
 
195
225
  it("identity.localhostUrl with portIdentity option", async () => {
196
- const c = cmd("docker compose").identity("infra");
197
- const urlToken = c.identity.localhostUrl("/health", { portIdentity: "minio" });
198
- const url = await (urlToken as (ctx: ComputeContext) => Promise<string>)(ctx);
226
+ const c = cmd("docker compose").withIdentity("infra");
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
  });
@@ -216,7 +252,7 @@ describe("multiple waitFor", () => {
216
252
 
217
253
  it("accepts callback form for multiple waitFor", async () => {
218
254
  const c = cmd("node server.js")
219
- .identity("api")
255
+ .withIdentity("api")
220
256
  .waitFor((self) => health.http(self.identity.url("/livez")))
221
257
  .waitFor((self) => health.tcp("localhost", self.identity.port()));
222
258
  const built = await c.build(ctx);
@@ -227,7 +263,7 @@ describe("multiple waitFor", () => {
227
263
  describe("waitFor callback form", () => {
228
264
  it("accepts a callback that receives the command", async () => {
229
265
  const c = cmd("node server.js")
230
- .identity("api")
266
+ .withIdentity("api")
231
267
  .dir("apps/api")
232
268
  .waitFor((self) => health.http(self.identity.url("/livez")));
233
269
 
@@ -240,7 +276,7 @@ describe("waitFor callback form", () => {
240
276
  describe("env callback form", () => {
241
277
  it("accepts a callback that receives the command", async () => {
242
278
  const c = cmd("node server.js")
243
- .identity("api")
279
+ .withIdentity("api")
244
280
  .env((self) => ({ PORT: self.identity.port() }));
245
281
 
246
282
  const built = await c.build(ctx);
@@ -251,7 +287,7 @@ describe("env callback form", () => {
251
287
 
252
288
  it("mixes callback env with record env", async () => {
253
289
  const c = cmd("node server.js")
254
- .identity("api")
290
+ .withIdentity("api")
255
291
  .env({ A: "1" })
256
292
  .env((self) => ({ PORT: self.identity.port() }))
257
293
  .env({ B: "2" });
@@ -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,
@@ -14,7 +14,6 @@ import {
14
14
  type HealthCheck,
15
15
  type HealthCheckCallback,
16
16
  type ICommand,
17
- type IdentityAccessor,
18
17
  isEnvCallback,
19
18
  isLazyEnvSource,
20
19
  type Runnable,
@@ -31,6 +30,7 @@ interface CommandState {
31
30
  readonly flags: readonly { name: string; value?: Token }[];
32
31
  readonly healthChecks: readonly HealthCheck[];
33
32
  readonly identitySuffix: string | null;
33
+ readonly logsSilent: boolean;
34
34
  }
35
35
 
36
36
  class Command implements ICommand {
@@ -41,24 +41,39 @@ class Command implements ICommand {
41
41
  this.state = state;
42
42
  }
43
43
 
44
- get identity(): IdentityAccessor {
45
- const id = createIdentity(
46
- this.state.identitySuffix ?? this.deriveName(),
47
- );
48
- const self = this;
49
- const setter = (suffix: string): ICommand => {
50
- return new Command({ ...self.state, identitySuffix: suffix });
51
- };
52
- // Function.name is read-only, must use defineProperty
53
- Object.defineProperty(setter, "name", {
54
- configurable: true,
55
- value: id.name,
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;
66
+ }
67
+
68
+ withIdentity(suffix: string): ICommand {
69
+ return new Command({ ...this.state, identitySuffix: suffix });
70
+ }
71
+
72
+ logs(options: { silent?: boolean }): ICommand {
73
+ return new Command({
74
+ ...this.state,
75
+ logsSilent: options.silent ?? false,
56
76
  });
57
- return Object.assign(setter, {
58
- localhostUrl: id.localhostUrl,
59
- port: id.port,
60
- url: id.url,
61
- }) as IdentityAccessor;
62
77
  }
63
78
 
64
79
  flag(name: string, value?: Token): ICommand {
@@ -194,9 +209,7 @@ class Command implements ICommand {
194
209
  rtx.logger.system(` env: ${JSON.stringify(built.env)}`);
195
210
  }
196
211
  if (this.state.healthChecks.length > 0) {
197
- const types = this.state.healthChecks
198
- .map((hc) => hc.type)
199
- .join(", ");
212
+ const types = this.state.healthChecks.map((hc) => hc.type).join(", ");
200
213
  rtx.logger.system(` waitFor: ${types}`);
201
214
  }
202
215
  if (this.state.dependencies.length > 0) {
@@ -226,17 +239,19 @@ class Command implements ICommand {
226
239
  });
227
240
 
228
241
  proc.onOutput((line, stream) => {
229
- rtx.logger.output(name, line, stream);
230
- rtx.eventBus.emit({
231
- data: {
232
- id: name,
233
- line,
234
- serviceName: name,
235
- stream,
236
- timestamp: Date.now(),
237
- },
238
- type: "log",
239
- });
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
+ }
240
255
  });
241
256
 
242
257
  if (this.state.healthChecks.length > 0) {
@@ -330,5 +345,6 @@ export function cmd(base: string): ICommand {
330
345
  flags: [],
331
346
  healthChecks: [],
332
347
  identitySuffix: null,
348
+ logsSilent: false,
333
349
  });
334
350
  }
@@ -29,7 +29,6 @@ export type {
29
29
  HealthCheckCallback,
30
30
  ICommand,
31
31
  ICompound,
32
- IdentityAccessor,
33
32
  LazyEnvSource,
34
33
  ProcessStatus,
35
34
  Runnable,
@@ -46,10 +46,6 @@ export interface BuiltCommand {
46
46
  env: Record<string, string>;
47
47
  }
48
48
 
49
- export interface IdentityAccessor extends Identity {
50
- (suffix: string): ICommand;
51
- }
52
-
53
49
  export interface ICommand {
54
50
  readonly __type: "command";
55
51
  arg(value: Token): ICommand;
@@ -60,9 +56,11 @@ export interface ICommand {
60
56
  dir(path: string): ICommand;
61
57
  env(source: EnvSource): ICommand;
62
58
  flag(name: string, value?: Token): ICommand;
63
- readonly identity: IdentityAccessor;
59
+ readonly identity: Identity;
60
+ logs(options: { silent?: boolean }): ICommand;
64
61
  run(): Promise<void>;
65
62
  waitFor(check: HealthCheck | HealthCheckCallback): ICommand;
63
+ withIdentity(suffix: string): ICommand;
66
64
  }
67
65
 
68
66
  // ─── Compound ────────────────────────────────────────────────────────────────
@@ -25,7 +25,6 @@ export type {
25
25
  HealthCheckCallback,
26
26
  ICommand,
27
27
  ICompound,
28
- IdentityAccessor,
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,
@@ -6,14 +6,16 @@ import type {
6
6
  HealthCheck,
7
7
  HealthCheckCallback,
8
8
  ICommand,
9
- IdentityAccessor,
10
9
  Runnable,
11
10
  Token,
12
11
  } from "../core/types.js";
12
+ 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;
18
+ withIdentity(suffix: string): IDockerCompose;
17
19
  }
18
20
 
19
21
  interface DockerComposeState {
@@ -49,24 +51,16 @@ class DockerCompose implements IDockerCompose {
49
51
 
50
52
  // ─── ICommand delegation (returns IDockerCompose for chaining) ───────
51
53
 
52
- get identity(): IdentityAccessor {
53
- const innerAccessor = this.inner.identity;
54
- const self = this;
55
- const setter = (suffix: string): IDockerCompose => {
56
- return new DockerCompose(
57
- self.inner.identity(suffix),
58
- self.dcState,
59
- );
60
- };
61
- Object.defineProperty(setter, "name", {
62
- configurable: true,
63
- value: innerAccessor.name,
64
- });
65
- return Object.assign(setter, {
66
- localhostUrl: innerAccessor.localhostUrl,
67
- port: innerAccessor.port,
68
- url: innerAccessor.url,
69
- }) as IdentityAccessor;
54
+ get identity(): Identity {
55
+ return this.inner.identity;
56
+ }
57
+
58
+ withIdentity(suffix: string): IDockerCompose {
59
+ return new DockerCompose(this.inner.withIdentity(suffix), this.dcState);
60
+ }
61
+
62
+ logs(options: { silent?: boolean }): IDockerCompose {
63
+ return new DockerCompose(this.inner.logs(options), this.dcState);
70
64
  }
71
65
 
72
66
  flag(name: string, value?: Token): IDockerCompose {
@@ -94,10 +88,7 @@ class DockerCompose implements IDockerCompose {
94
88
  }
95
89
 
96
90
  dependsOn(...deps: Runnable[]): IDockerCompose {
97
- return new DockerCompose(
98
- this.inner.dependsOn(...deps),
99
- this.dcState,
100
- );
91
+ return new DockerCompose(this.inner.dependsOn(...deps), this.dcState);
101
92
  }
102
93
 
103
94
  collectNames(): string[] {
@@ -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";
@@ -6,12 +6,15 @@ import type {
6
6
  HealthCheck,
7
7
  HealthCheckCallback,
8
8
  ICommand,
9
- IdentityAccessor,
10
9
  Runnable,
11
10
  Token,
12
11
  } from "../core/types.js";
12
+ import type { Identity } from "../utils/dna.js";
13
13
 
14
- export interface INextjs extends ICommand {}
14
+ export interface INextjs extends ICommand {
15
+ logs(options: { silent?: boolean }): INextjs;
16
+ withIdentity(suffix: string): INextjs;
17
+ }
15
18
 
16
19
  /** @deprecated Use `INextjs` instead */
17
20
  export type NextPreset = INextjs;
@@ -26,21 +29,16 @@ class Nextjs implements INextjs {
26
29
 
27
30
  // ─── ICommand delegation (returns INextjs for chaining) ─────────────
28
31
 
29
- get identity(): IdentityAccessor {
30
- const innerAccessor = this.inner.identity;
31
- const self = this;
32
- const setter = (suffix: string): INextjs => {
33
- return new Nextjs(self.inner.identity(suffix));
34
- };
35
- Object.defineProperty(setter, "name", {
36
- configurable: true,
37
- value: innerAccessor.name,
38
- });
39
- return Object.assign(setter, {
40
- localhostUrl: innerAccessor.localhostUrl,
41
- port: innerAccessor.port,
42
- url: innerAccessor.url,
43
- }) as IdentityAccessor;
32
+ get identity(): Identity {
33
+ return this.inner.identity;
34
+ }
35
+
36
+ withIdentity(suffix: string): INextjs {
37
+ return new Nextjs(this.inner.withIdentity(suffix));
38
+ }
39
+
40
+ logs(options: { silent?: boolean }): INextjs {
41
+ return new Nextjs(this.inner.logs(options));
44
42
  }
45
43
 
46
44
  flag(name: string, value?: Token): INextjs {
@@ -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;