@buildepicshit/cli 0.0.2 → 0.0.3

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.
@@ -0,0 +1,62 @@
1
+ // Core API
2
+ export {
3
+ cmd,
4
+ compute,
5
+ customHealthCheck,
6
+ execHealthCheck,
7
+ fromEnv,
8
+ fromFile,
9
+ fromPackageJson,
10
+ health,
11
+ httpHealthCheck,
12
+ par,
13
+ seq,
14
+ stdoutHealthCheck,
15
+ tcpHealthCheck,
16
+ } from "./core/index.js";
17
+ // Types
18
+ export type {
19
+ BuiltCommand,
20
+ ComputeContext,
21
+ EnvCallback,
22
+ EnvSource,
23
+ HealthCheck,
24
+ HealthCheckCallback,
25
+ ICommand,
26
+ ICompound,
27
+ ProcessStatus,
28
+ Runnable,
29
+ Token,
30
+ } from "./core/types.js";
31
+ // Logger
32
+ export type { Logger } from "./logger/logger.js";
33
+ export { createLazyLogger, createLogger } from "./logger/logger.js";
34
+ // Presets
35
+ export type { NextPreset } from "./presets/index.js";
36
+ export {
37
+ docker,
38
+ esbuild,
39
+ esbuildWatch,
40
+ hono,
41
+ next,
42
+ nodemon,
43
+ tsx,
44
+ } from "./presets/index.js";
45
+ // Runner
46
+ export type {
47
+ DevEvent,
48
+ IEventBus,
49
+ LogEntry,
50
+ OrchestratorProcess,
51
+ ProcessMeta,
52
+ RuntimeContext,
53
+ } from "./runner/index.js";
54
+ export {
55
+ clearRuntimeContext,
56
+ createEventBus,
57
+ getRuntimeContext,
58
+ setRuntimeContext,
59
+ } from "./runner/index.js";
60
+ export type { Identity } from "./utils/dna.js";
61
+ // Utils
62
+ export { createIdentity, identity } from "./utils/dna.js";
@@ -0,0 +1,123 @@
1
+ import chalk, { type ChalkInstance } from "chalk";
2
+
3
+ const COLORS: ChalkInstance[] = [
4
+ chalk.cyan,
5
+ chalk.magenta,
6
+ chalk.yellow,
7
+ chalk.green,
8
+ chalk.blue,
9
+ chalk.red,
10
+ chalk.white,
11
+ ];
12
+
13
+ export interface Logger {
14
+ completed(serviceName: string): void;
15
+ error(serviceName: string, message: string): void;
16
+ healthy(serviceName: string): void;
17
+ output(serviceName: string, line: string, stream: "stderr" | "stdout"): void;
18
+ ready(serviceName: string): void;
19
+ starting(serviceName: string, nodeId: string): void;
20
+ system(message: string): void;
21
+ }
22
+
23
+ export function createLogger(serviceNames: string[]): Logger {
24
+ // Compute max prefix width for alignment
25
+ const maxLen = Math.max(...serviceNames.map((n) => n.length), 6);
26
+ const colorMap = new Map<string, ChalkInstance>();
27
+
28
+ for (let i = 0; i < serviceNames.length; i++) {
29
+ colorMap.set(serviceNames[i], COLORS[i % COLORS.length]);
30
+ }
31
+
32
+ function prefix(name: string): string {
33
+ const color = colorMap.get(name) ?? chalk.white;
34
+ return color(`[${name.padEnd(maxLen)}]`);
35
+ }
36
+
37
+ return {
38
+ completed(serviceName) {
39
+ console.log(`${prefix(serviceName)} ${chalk.green("completed")} ✓`);
40
+ },
41
+
42
+ error(serviceName, message) {
43
+ console.error(
44
+ `${prefix(serviceName)} ${chalk.red("error")} ✗ ${message}`,
45
+ );
46
+ },
47
+
48
+ healthy(serviceName) {
49
+ console.log(`${prefix(serviceName)} ${chalk.green("healthy")} ✓`);
50
+ },
51
+
52
+ output(serviceName, line, _stream) {
53
+ console.log(`${prefix(serviceName)} ${line}`);
54
+ },
55
+
56
+ ready(serviceName) {
57
+ console.log(`${prefix(serviceName)} ${chalk.green("ready")} ✓`);
58
+ },
59
+ starting(serviceName, nodeId) {
60
+ console.log(`${prefix(serviceName)} Starting ${nodeId}...`);
61
+ },
62
+
63
+ system(message) {
64
+ console.log(chalk.gray(message));
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Lazy logger that auto-registers service names on first use.
71
+ * Adjusts column padding dynamically as new names appear.
72
+ */
73
+ export function createLazyLogger(): Logger {
74
+ const colorMap = new Map<string, ChalkInstance>();
75
+ let maxLen = 6;
76
+ let nextColorIdx = 0;
77
+
78
+ function ensureName(name: string) {
79
+ if (!colorMap.has(name)) {
80
+ colorMap.set(name, COLORS[nextColorIdx % COLORS.length]);
81
+ nextColorIdx++;
82
+ maxLen = Math.max(maxLen, name.length);
83
+ }
84
+ }
85
+
86
+ function prefix(name: string): string {
87
+ ensureName(name);
88
+ const color = colorMap.get(name) as ChalkInstance;
89
+ return color(`[${name.padEnd(maxLen)}]`);
90
+ }
91
+
92
+ return {
93
+ completed(serviceName) {
94
+ console.log(`${prefix(serviceName)} ${chalk.green("completed")} ✓`);
95
+ },
96
+
97
+ error(serviceName, message) {
98
+ console.error(
99
+ `${prefix(serviceName)} ${chalk.red("error")} ✗ ${message}`,
100
+ );
101
+ },
102
+
103
+ healthy(serviceName) {
104
+ console.log(`${prefix(serviceName)} ${chalk.green("healthy")} ✓`);
105
+ },
106
+
107
+ output(serviceName, line, _stream) {
108
+ console.log(`${prefix(serviceName)} ${line}`);
109
+ },
110
+
111
+ ready(serviceName) {
112
+ console.log(`${prefix(serviceName)} ${chalk.green("ready")} ✓`);
113
+ },
114
+
115
+ starting(serviceName, _nodeId) {
116
+ console.log(`${prefix(serviceName)} Starting...`);
117
+ },
118
+
119
+ system(message) {
120
+ console.log(chalk.gray(message));
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,26 @@
1
+ import { cmd } from "../core/command.js";
2
+ import type { ICommand, Token } from "../core/types.js";
3
+
4
+ export function docker(options?: {
5
+ detach?: boolean;
6
+ file?: Token;
7
+ service?: string;
8
+ }): ICommand {
9
+ let command = cmd("docker compose");
10
+
11
+ if (options?.file) {
12
+ command = command.flag("file", options.file);
13
+ }
14
+
15
+ command = command.arg("up");
16
+
17
+ if (options?.detach) {
18
+ command = command.flag("detach");
19
+ }
20
+
21
+ if (options?.service) {
22
+ command = command.arg(options.service);
23
+ }
24
+
25
+ return command;
26
+ }
@@ -0,0 +1,12 @@
1
+ import { cmd } from "../core/command.js";
2
+ import type { ICommand } from "../core/types.js";
3
+
4
+ export function esbuild(configFile?: string): ICommand {
5
+ return cmd("node").arg(configFile ?? "esbuild.config.js");
6
+ }
7
+
8
+ export function esbuildWatch(configFile?: string): ICommand {
9
+ return cmd("node")
10
+ .arg(configFile ?? "esbuild.config.js")
11
+ .flag("watch");
12
+ }
@@ -0,0 +1,7 @@
1
+ import { cmd } from "../core/command.js";
2
+ import type { ICommand } from "../core/types.js";
3
+ import { identity } from "../utils/dna.js";
4
+
5
+ export function hono(): ICommand {
6
+ return cmd("tsx").env({ PORT: identity("hono").port() });
7
+ }
@@ -0,0 +1,5 @@
1
+ export { docker } from "./docker.js";
2
+ export { esbuild, esbuildWatch } from "./esbuild.js";
3
+ export { hono } from "./hono.js";
4
+ export { type NextPreset, next } from "./nextjs.js";
5
+ export { nodemon, tsx } from "./node.js";
@@ -0,0 +1,48 @@
1
+ import { cmd } from "../core/command";
2
+ import { health } from "../core/health-check";
3
+ import type { ICommand } from "../core/types";
4
+ import { createIdentity } from "../utils/dna";
5
+
6
+ export interface NextPreset {
7
+ build(): ICommand;
8
+ dev(): ICommand;
9
+ dir(path: string): NextPreset;
10
+ start(): ICommand;
11
+ }
12
+
13
+ export function next(suffix = "next"): NextPreset {
14
+ const identity = createIdentity(suffix);
15
+ return createNextPreset(identity, null);
16
+ }
17
+
18
+ function createNextPreset(
19
+ identity: ReturnType<typeof createIdentity>,
20
+ dirPath: string | null,
21
+ ): NextPreset {
22
+ function applyDir(command: ICommand): ICommand {
23
+ return dirPath ? command.dir(dirPath) : command;
24
+ }
25
+
26
+ return {
27
+ build(): ICommand {
28
+ return applyDir(cmd("next build"));
29
+ },
30
+ dev(): ICommand {
31
+ return applyDir(
32
+ cmd("next dev")
33
+ .waitFor(health.http(identity.url("/livez")))
34
+ .env({ PORT: identity.port() }),
35
+ );
36
+ },
37
+ dir(path: string): NextPreset {
38
+ return createNextPreset(identity, path);
39
+ },
40
+ start(): ICommand {
41
+ return applyDir(
42
+ cmd("next start")
43
+ .waitFor(health.http(identity.url("/livez")))
44
+ .env({ PORT: identity.port() }),
45
+ );
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,12 @@
1
+ import { cmd } from "../core/command.js";
2
+ import type { ICommand } from "../core/types.js";
3
+
4
+ export function tsx(entry: string, watch?: boolean): ICommand {
5
+ let command = cmd("tsx");
6
+ if (watch) command = command.arg("watch");
7
+ return command.arg(entry);
8
+ }
9
+
10
+ export function nodemon(entry: string): ICommand {
11
+ return cmd("nodemon").arg(entry);
12
+ }
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createEventBus, type DevEvent } from "../event-bus.js";
3
+
4
+ describe("createEventBus", () => {
5
+ it("emits events to listeners", () => {
6
+ const bus = createEventBus();
7
+ const listener = vi.fn();
8
+ bus.on(listener);
9
+
10
+ const event: DevEvent = { type: "stack:ready" };
11
+ bus.emit(event);
12
+
13
+ expect(listener).toHaveBeenCalledWith(event);
14
+ });
15
+
16
+ it("supports multiple listeners", () => {
17
+ const bus = createEventBus();
18
+ const l1 = vi.fn();
19
+ const l2 = vi.fn();
20
+ bus.on(l1);
21
+ bus.on(l2);
22
+
23
+ bus.emit({ type: "stack:ready" });
24
+
25
+ expect(l1).toHaveBeenCalledOnce();
26
+ expect(l2).toHaveBeenCalledOnce();
27
+ });
28
+
29
+ it("removes listeners with off()", () => {
30
+ const bus = createEventBus();
31
+ const listener = vi.fn();
32
+ bus.on(listener);
33
+ bus.off(listener);
34
+
35
+ bus.emit({ type: "stack:ready" });
36
+
37
+ expect(listener).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it("emits process:registered with meta", () => {
41
+ const bus = createEventBus();
42
+ const listener = vi.fn();
43
+ bus.on(listener);
44
+
45
+ bus.emit({
46
+ data: {
47
+ command: "next dev --port 3456",
48
+ cwd: "/apps/web",
49
+ id: "web:dev",
50
+ serviceName: "web",
51
+ },
52
+ type: "process:registered",
53
+ });
54
+
55
+ expect(listener).toHaveBeenCalledWith(
56
+ expect.objectContaining({ type: "process:registered" }),
57
+ );
58
+ });
59
+
60
+ it("emits log entries with timestamp", () => {
61
+ const bus = createEventBus();
62
+ const events: DevEvent[] = [];
63
+ bus.on((e) => events.push(e));
64
+
65
+ bus.emit({
66
+ data: {
67
+ id: "api:dev",
68
+ line: "Server listening on port 3000",
69
+ serviceName: "api",
70
+ stream: "stdout",
71
+ timestamp: Date.now(),
72
+ },
73
+ type: "log",
74
+ });
75
+
76
+ expect(events).toHaveLength(1);
77
+ expect(events[0].type).toBe("log");
78
+ });
79
+
80
+ it("emits stack:failed with error details", () => {
81
+ const bus = createEventBus();
82
+ const listener = vi.fn();
83
+ bus.on(listener);
84
+
85
+ bus.emit({
86
+ data: { error: "Process crashed", service: "api" },
87
+ type: "stack:failed",
88
+ });
89
+
90
+ expect(listener).toHaveBeenCalledWith(
91
+ expect.objectContaining({
92
+ data: { error: "Process crashed", service: "api" },
93
+ type: "stack:failed",
94
+ }),
95
+ );
96
+ });
97
+ });
@@ -0,0 +1,55 @@
1
+ import type { ProcessStatus } from "../core/types.js";
2
+
3
+ // ─── Event payload types ─────────────────────────────────────────────────────
4
+
5
+ export interface ProcessMeta {
6
+ command: string;
7
+ cwd: string;
8
+ id: string;
9
+ serviceName: string;
10
+ }
11
+
12
+ export interface LogEntry {
13
+ id: string;
14
+ line: string;
15
+ serviceName: string;
16
+ stream: "stderr" | "stdout";
17
+ timestamp: number;
18
+ }
19
+
20
+ // ─── Event union ─────────────────────────────────────────────────────────────
21
+
22
+ export type DevEvent =
23
+ | { data: LogEntry; type: "log" }
24
+ | { data: ProcessMeta; type: "process:registered" }
25
+ | { data: { id: string; status: ProcessStatus }; type: "process:status" }
26
+ | { data: { error: string; service: string }; type: "stack:failed" }
27
+ | { type: "stack:ready" };
28
+
29
+ // ─── EventBus ────────────────────────────────────────────────────────────────
30
+
31
+ type Listener = (event: DevEvent) => void;
32
+
33
+ export interface IEventBus {
34
+ emit(event: DevEvent): void;
35
+ off(listener: Listener): void;
36
+ on(listener: Listener): void;
37
+ }
38
+
39
+ export function createEventBus(): IEventBus {
40
+ const listeners = new Set<Listener>();
41
+
42
+ return {
43
+ emit(event: DevEvent): void {
44
+ for (const listener of listeners) {
45
+ listener(event);
46
+ }
47
+ },
48
+ off(listener: Listener): void {
49
+ listeners.delete(listener);
50
+ },
51
+ on(listener: Listener): void {
52
+ listeners.add(listener);
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,129 @@
1
+ import { createConnection } from "node:net";
2
+ import { resolveToken } from "../core/token.js";
3
+ import type { ComputeContext, HealthCheck } from "../core/types.js";
4
+ import type { OrchestratorProcess } from "./process.js";
5
+
6
+ interface HealthCheckOptions {
7
+ intervalMs?: number;
8
+ maxRetries?: number;
9
+ timeoutMs?: number;
10
+ }
11
+
12
+ const DEFAULT_INTERVAL = 1000;
13
+ const DEFAULT_TIMEOUT = 30_000;
14
+ const DEFAULT_RETRIES = 30;
15
+
16
+ export async function waitForHealthy(
17
+ check: HealthCheck,
18
+ ctx: ComputeContext,
19
+ proc: OrchestratorProcess,
20
+ options: HealthCheckOptions = {},
21
+ ): Promise<void> {
22
+ const interval = options.intervalMs ?? DEFAULT_INTERVAL;
23
+ const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT;
24
+ const maxRetries = options.maxRetries ?? DEFAULT_RETRIES;
25
+
26
+ const startTime = Date.now();
27
+ let retries = 0;
28
+
29
+ while (retries < maxRetries && Date.now() - startTime < timeout) {
30
+ // Bail if process died
31
+ if (proc.status === "failed" || proc.status === "completed") {
32
+ throw new Error(
33
+ `Process "${proc.serviceName}" exited before becoming healthy`,
34
+ );
35
+ }
36
+
37
+ try {
38
+ const healthy = await runCheck(check, ctx, proc);
39
+ if (healthy) return;
40
+ } catch {
41
+ // Check failed, retry
42
+ }
43
+
44
+ retries++;
45
+ await sleep(interval);
46
+ }
47
+
48
+ throw new Error(
49
+ `Health check for "${proc.serviceName}" timed out after ${timeout}ms (${retries} retries)`,
50
+ );
51
+ }
52
+
53
+ async function runCheck(
54
+ check: HealthCheck,
55
+ ctx: ComputeContext,
56
+ proc: OrchestratorProcess,
57
+ ): Promise<boolean> {
58
+ switch (check.type) {
59
+ case "tcp":
60
+ return checkTcp(
61
+ await resolveToken(check.host, ctx),
62
+ Number.parseInt(await resolveToken(check.port, ctx), 10),
63
+ );
64
+
65
+ case "http": {
66
+ const resolvedUrl = await resolveToken(check.url, ctx);
67
+ return checkHttp(resolvedUrl, check.status);
68
+ }
69
+
70
+ case "stdout":
71
+ return checkStdout(check.pattern, proc);
72
+
73
+ case "exec": {
74
+ const built = await check.command.build(ctx);
75
+ const fullCmd = [built.command, ...built.args].join(" ");
76
+ const { execa } = await import("execa");
77
+ const result = await execa({ reject: false, shell: true })`${fullCmd}`;
78
+ return result.exitCode === 0;
79
+ }
80
+
81
+ case "custom":
82
+ return check.check();
83
+
84
+ default:
85
+ throw new Error(
86
+ `Unknown health check type: ${(check as { type: string }).type}`,
87
+ );
88
+ }
89
+ }
90
+
91
+ function checkTcp(host: string, port: number): Promise<boolean> {
92
+ return new Promise((resolve) => {
93
+ const socket = createConnection({ host, port }, () => {
94
+ socket.destroy();
95
+ resolve(true);
96
+ });
97
+ socket.on("error", () => {
98
+ socket.destroy();
99
+ resolve(false);
100
+ });
101
+ socket.setTimeout(2000, () => {
102
+ socket.destroy();
103
+ resolve(false);
104
+ });
105
+ });
106
+ }
107
+
108
+ async function checkHttp(
109
+ url: string,
110
+ expectedStatus?: number,
111
+ ): Promise<boolean> {
112
+ try {
113
+ const response = await fetch(url, {
114
+ signal: AbortSignal.timeout(2000),
115
+ });
116
+ if (expectedStatus) return response.status === expectedStatus;
117
+ return response.ok;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ function checkStdout(pattern: RegExp, proc: OrchestratorProcess): boolean {
124
+ return proc.outputLines.some((line) => pattern.test(line));
125
+ }
126
+
127
+ function sleep(ms: number): Promise<void> {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ createEventBus,
3
+ type DevEvent,
4
+ type IEventBus,
5
+ type LogEntry,
6
+ type ProcessMeta,
7
+ } from "./event-bus.js";
8
+ export { waitForHealthy } from "./health-runner.js";
9
+ export { type OrchestratorProcess, spawnProcess } from "./process.js";
10
+ export {
11
+ clearRuntimeContext,
12
+ createDeferred,
13
+ type Deferred,
14
+ getRuntimeContext,
15
+ type RuntimeContext,
16
+ setRuntimeContext,
17
+ } from "./runtime-context.js";