feat(cli): port Fase 4 CLI — parser arg, mode dispatch, single-process composition root + REPL loop, bootstrap seed
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
> Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo
|
> Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo
|
||||||
> TypeScript/Bun. Bahasa Indonesia, commit pakai Conventional Commits.
|
> TypeScript/Bun. Bahasa Indonesia, commit pakai Conventional Commits.
|
||||||
>
|
>
|
||||||
> **Status:** Fase 0–2 + 3a + 3b + 3e + 3c + 3d + 3f + 3g **selesai**. Berikutnya: **4 gateway + CLI + TUI**.
|
> **Status:** Fase 0–2 + 3a–3g **selesai**; Fase 4 sebagian (**CLI**: parser arg, dispatch, komposisi root, REPL, bootstrap). Berikutnya: Fase 4 TUI + Fase 5 server interfaces.
|
||||||
> Base: `bun run check` bersih, 54 test hijau.
|
> Base: `bun run check` bersih, 54 test hijau.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -147,10 +147,12 @@ apps/
|
|||||||
### Fase 4 — Gateway + CLI + TUI
|
### Fase 4 — Gateway + CLI + TUI
|
||||||
**Sumber Rust:** `apps/gateway/src/{lib,main}.rs`, `apps/interfaces/tui/src/**`
|
**Sumber Rust:** `apps/gateway/src/{lib,main}.rs`, `apps/interfaces/tui/src/**`
|
||||||
|
|
||||||
- [ ] `apps/interfaces/cli/` (paket Bun):
|
- [x] `apps/interfaces/cli/` (paket Bun):
|
||||||
- [ ] Parser arg: `--api/--daemon/--attach/--ws/--grpc/--web/--tui`, dispatch mode
|
- [x] Parser arg: `--api/--daemon/--attach/--ws/--grpc/--web`, dispatch mode (`args.ts`)
|
||||||
- [ ] Logging ke file
|
- [x] Logging ke file (`index.ts`)
|
||||||
- [ ] `run_single_process` — komposisi root: wire semua implementasi konkret sekali
|
- [x] `run_single_process` — komposisi root: wire semua implementasi konkret sekali (`compose.ts`) + REPL loop
|
||||||
|
- [x] Bootstrap seed (`bootstrap.ts`) — wire-shape settings lama terbaca
|
||||||
|
- [x] Smoke: `bun run cli --help`, `bun run bootstrap` jalan; loop turn + tool read/write
|
||||||
- [ ] TUI mode (library TUI Bun):
|
- [ ] TUI mode (library TUI Bun):
|
||||||
- [ ] Event loop adaptive poll
|
- [ ] Event loop adaptive poll
|
||||||
- [ ] `Action` dispatcher + slash commands
|
- [ ] `Action` dispatcher + slash commands
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@zesdex/cli",
|
||||||
|
"version": "1.21.2",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"cli": "bun src/index.ts",
|
||||||
|
"build": "true",
|
||||||
|
"bootstrap": "bun src/bootstrap.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@zesdex/domain": "workspace:*",
|
||||||
|
"@zesdex/application": "workspace:*",
|
||||||
|
"@zesdex/infrastructure": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* CLI argument parsing and mode dispatch.
|
||||||
|
* Mirrors `apps/gateway/src/main.rs` (clap → manual Bun.argv parsing).
|
||||||
|
*
|
||||||
|
* Modes (mutually exclusive): --daemon, --attach <session>, --api, --ws,
|
||||||
|
* --grpc, --web; default is TUI single-process mode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CliOptions {
|
||||||
|
daemon: boolean;
|
||||||
|
attach: string | null;
|
||||||
|
api: boolean;
|
||||||
|
apiPort: number;
|
||||||
|
ws: boolean;
|
||||||
|
wsPort: number;
|
||||||
|
grpc: boolean;
|
||||||
|
grpcPort: number;
|
||||||
|
web: boolean;
|
||||||
|
webPort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HELP = `zesdex — autonomous AI coding agent with TUI
|
||||||
|
|
||||||
|
Usage: zesdex [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--daemon Run as background daemon with IPC socket
|
||||||
|
--attach <session> Attach TUI client to a running daemon session
|
||||||
|
--api Run REST API server
|
||||||
|
--api-port <PORT> REST API port [default: 8080]
|
||||||
|
--ws Run WebSocket server
|
||||||
|
--ws-port <PORT> WebSocket port [default: 8081]
|
||||||
|
--grpc Run gRPC server
|
||||||
|
--grpc-port <PORT> gRPC port [default: 50051]
|
||||||
|
--web Serve web frontend
|
||||||
|
--web-port <PORT> Web frontend port [default: 3000]
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
`;
|
||||||
|
|
||||||
|
/** Parse process argv (excluding node/bun) into CliOptions. */
|
||||||
|
export function parseCli(argv: string[]): CliOptions | { help: true; text: string } {
|
||||||
|
const opts: CliOptions = {
|
||||||
|
daemon: false,
|
||||||
|
attach: null,
|
||||||
|
api: false,
|
||||||
|
apiPort: 8080,
|
||||||
|
ws: false,
|
||||||
|
wsPort: 8081,
|
||||||
|
grpc: false,
|
||||||
|
grpcPort: 50051,
|
||||||
|
web: false,
|
||||||
|
webPort: 3000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sawFlag = false;
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i]!;
|
||||||
|
if (arg === "--help" || arg === "-h") {
|
||||||
|
return { help: true, text: HELP };
|
||||||
|
}
|
||||||
|
if (arg === "--version" || arg === "-V") {
|
||||||
|
return { help: true, text: `zesdex 1.21.2` };
|
||||||
|
}
|
||||||
|
const assignString = (key: keyof CliOptions, next: string | undefined, name: string) => {
|
||||||
|
if (next === undefined) throw new Error(`flag '${name}' requires a value`);
|
||||||
|
(opts as unknown as Record<string, unknown>)[key] = next;
|
||||||
|
};
|
||||||
|
const assignPort = (key: keyof CliOptions, next: string | undefined, name: string) => {
|
||||||
|
if (next === undefined) throw new Error(`flag '${name}' requires a value`);
|
||||||
|
const n = Number(next);
|
||||||
|
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
||||||
|
throw new Error(`invalid port for '${name}': ${next}`);
|
||||||
|
}
|
||||||
|
(opts as unknown as Record<string, unknown>)[key] = n;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (arg) {
|
||||||
|
case "--daemon":
|
||||||
|
opts.daemon = true;
|
||||||
|
sawFlag = true;
|
||||||
|
break;
|
||||||
|
case "--attach":
|
||||||
|
assignString("attach", argv[++i], "--attach");
|
||||||
|
sawFlag = true;
|
||||||
|
break;
|
||||||
|
case "--api":
|
||||||
|
opts.api = true;
|
||||||
|
sawFlag = true;
|
||||||
|
break;
|
||||||
|
case "--api-port":
|
||||||
|
assignPort("apiPort", argv[++i], "--api-port");
|
||||||
|
break;
|
||||||
|
case "--ws":
|
||||||
|
opts.ws = true;
|
||||||
|
sawFlag = true;
|
||||||
|
break;
|
||||||
|
case "--ws-port":
|
||||||
|
assignPort("wsPort", argv[++i], "--ws-port");
|
||||||
|
break;
|
||||||
|
case "--grpc":
|
||||||
|
opts.grpc = true;
|
||||||
|
sawFlag = true;
|
||||||
|
break;
|
||||||
|
case "--grpc-port":
|
||||||
|
assignPort("grpcPort", argv[++i], "--grpc-port");
|
||||||
|
break;
|
||||||
|
case "--web":
|
||||||
|
opts.web = true;
|
||||||
|
sawFlag = true;
|
||||||
|
break;
|
||||||
|
case "--web-port":
|
||||||
|
assignPort("webPort", argv[++i], "--web-port");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`unexpected argument '${arg}'\n\n${HELP}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void sawFlag;
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate that at most one mode flag is set. */
|
||||||
|
export function modeCount(opts: CliOptions): number {
|
||||||
|
let count = opts.daemon ? 1 : 0;
|
||||||
|
if (opts.api) count++;
|
||||||
|
if (opts.ws) count++;
|
||||||
|
if (opts.grpc) count++;
|
||||||
|
if (opts.web) count++;
|
||||||
|
if (opts.attach !== null) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Bootstrap seed — create the initial settings.json / app_config.json if missing.
|
||||||
|
* Mirrors the Rust `bootstrap` seed.
|
||||||
|
*/
|
||||||
|
import { newStore, ensureStoreDirs } from "@zesdex/domain";
|
||||||
|
import { JsonSettingsRepository, JsonAppConfigRepository } from "@zesdex/infrastructure";
|
||||||
|
|
||||||
|
/** Seed default settings + app config into the store directory. */
|
||||||
|
export async function bootstrap(): Promise<void> {
|
||||||
|
const store = newStore();
|
||||||
|
await ensureStoreDirs(store);
|
||||||
|
|
||||||
|
const settingsRepo = new JsonSettingsRepository();
|
||||||
|
const appConfigRepo = new JsonAppConfigRepository();
|
||||||
|
|
||||||
|
const settings = await settingsRepo.load(store.base_dir);
|
||||||
|
const appConfig = await appConfigRepo.load(store.base_dir);
|
||||||
|
|
||||||
|
if (!settings.api_keys[settings.provider]) {
|
||||||
|
console.info(`provider '${settings.provider}' has no API key configured.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`seeded defaults at ${store.base_dir}`);
|
||||||
|
console.info(` provider: ${settings.provider}`);
|
||||||
|
console.info(` model: ${settings.model}`);
|
||||||
|
console.info(` providers: ${Object.keys(appConfig.providers).join(", ") || "(none)"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run when invoked directly as `bun src/bootstrap.ts`.
|
||||||
|
if (import.meta.main) {
|
||||||
|
bootstrap().catch((e) => {
|
||||||
|
console.error(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Single-process composition root — wires every concrete implementation once.
|
||||||
|
* Mirrors the `run_single_process` composition in the Rust gateway/TUI.
|
||||||
|
*
|
||||||
|
* Assembles: Store → Settings/AppConfig repos → LLM provider → ToolCtx +
|
||||||
|
* executor → AgentTurnServiceImpl.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
newStore,
|
||||||
|
ensureStoreDirs,
|
||||||
|
type AgentTurnParams,
|
||||||
|
type TurnEvent,
|
||||||
|
type TurnEventSink,
|
||||||
|
type ChatMessage,
|
||||||
|
type Store,
|
||||||
|
userMessage,
|
||||||
|
resolveEffectiveModel,
|
||||||
|
} from "@zesdex/domain";
|
||||||
|
import {
|
||||||
|
JsonSettingsRepository,
|
||||||
|
JsonAppConfigRepository,
|
||||||
|
LlmClient,
|
||||||
|
resolveApiKey,
|
||||||
|
InfrastructureToolExecutor,
|
||||||
|
allTools,
|
||||||
|
toolDefs,
|
||||||
|
} from "@zesdex/infrastructure";
|
||||||
|
import type { ToolCtx } from "@zesdex/infrastructure";
|
||||||
|
import { AgentTurnServiceImpl } from "@zesdex/application";
|
||||||
|
|
||||||
|
/** Wired runtime for single-process mode. */
|
||||||
|
export interface WiredRuntime {
|
||||||
|
store: Store;
|
||||||
|
executor: InfrastructureToolExecutor;
|
||||||
|
turnService: AgentTurnServiceImpl;
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
apiBase?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire all layers together once.
|
||||||
|
* Returns the assembled runtime that both the REPL loop and future
|
||||||
|
* interface servers share.
|
||||||
|
*/
|
||||||
|
export async function runSingleProcess(): Promise<WiredRuntime> {
|
||||||
|
const store = newStore();
|
||||||
|
await ensureStoreDirs(store);
|
||||||
|
|
||||||
|
// Load settings + app config from the store directory (repos are async).
|
||||||
|
const settingsRepo = new JsonSettingsRepository();
|
||||||
|
const appConfigRepo = new JsonAppConfigRepository();
|
||||||
|
const settings = await settingsRepo.load(store.base_dir);
|
||||||
|
const appConfig = await appConfigRepo.load(store.base_dir);
|
||||||
|
|
||||||
|
// Resolve the effective provider / model / base URL / API key.
|
||||||
|
const provider = settings.provider;
|
||||||
|
const model = resolveEffectiveModel(settings, appConfig);
|
||||||
|
const baseUrl = appConfig.providers[provider]?.api_base ?? undefined;
|
||||||
|
const apiKey = resolveApiKey(settings, appConfig);
|
||||||
|
|
||||||
|
// Build the LLM provider service (implements the ProviderService port).
|
||||||
|
const llmClient = new LlmClient(apiKey, model, baseUrl);
|
||||||
|
|
||||||
|
// Build the tool context + executor.
|
||||||
|
const executor = new InfrastructureToolExecutor(buildToolCtx(store));
|
||||||
|
|
||||||
|
// Assemble the turn service (provider + executor + tool definitions).
|
||||||
|
const turnService = new AgentTurnServiceImpl(
|
||||||
|
llmClient,
|
||||||
|
executor,
|
||||||
|
toolDefs(allTools()) as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { store, executor, turnService, apiKey, model, apiBase: baseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rooted, long-lived event sink that collects events into an array. */
|
||||||
|
export class ArrayTurnEventSink implements TurnEventSink {
|
||||||
|
constructor(public events: TurnEvent[] = []) {}
|
||||||
|
push(event: TurnEvent): void {
|
||||||
|
this.events.push(event);
|
||||||
|
}
|
||||||
|
drain(): TurnEvent[] {
|
||||||
|
const out = this.events;
|
||||||
|
this.events = [];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a fresh set of turn params for a single user query. */
|
||||||
|
export function buildTurnParams(
|
||||||
|
runtime: WiredRuntime,
|
||||||
|
userText: string,
|
||||||
|
events: TurnEvent[],
|
||||||
|
abort: AbortController,
|
||||||
|
): AgentTurnParams {
|
||||||
|
const sink = new ArrayTurnEventSink(events);
|
||||||
|
const messages: ChatMessage[] = [userMessage(userText)];
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
session_dir: runtime.store.base_dir,
|
||||||
|
workspace_roots: [process.cwd()],
|
||||||
|
turn_events: sink,
|
||||||
|
in_flight: { value: false },
|
||||||
|
abort,
|
||||||
|
api_key: runtime.apiKey,
|
||||||
|
model: runtime.model,
|
||||||
|
api_base: runtime.apiBase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a ToolCtx with the standard fields. */
|
||||||
|
function buildToolCtx(store: Store): ToolCtx {
|
||||||
|
return {
|
||||||
|
sessionDir: store.base_dir,
|
||||||
|
workspaces: [process.cwd()],
|
||||||
|
turnEvents: new ArrayTurnEventSink(),
|
||||||
|
workflowFindings: [],
|
||||||
|
} as unknown as ToolCtx;
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Zesdex CLI — main entry point.
|
||||||
|
* Mirrors `apps/gateway/src/main.rs`.
|
||||||
|
*
|
||||||
|
* Parses argv, sets up file logging, and dispatches to the requested mode.
|
||||||
|
* Default mode runs the single-process REPL loop.
|
||||||
|
*/
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { parseCli, modeCount } from "./args.ts";
|
||||||
|
import { runSingleProcess } from "./compose.ts";
|
||||||
|
|
||||||
|
/** Initialise file logging (append to <data-dir>/zesdex/zesdex.log). */
|
||||||
|
function initLogging(): void {
|
||||||
|
const dataDir = os.homedir();
|
||||||
|
const logDir = path.join(dataDir, ".local", "share", "zesdex");
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
const logPath = path.join(logDir, "zesdex.log");
|
||||||
|
const stream = fs.createWriteStream(logPath, { flags: "a" });
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
const originalInfo = console.info;
|
||||||
|
|
||||||
|
const write = (level: string, args: unknown[]) => {
|
||||||
|
const line = `[${new Date().toISOString()}] ${level} ${args
|
||||||
|
.map((a) => (typeof a === "string" ? a : JSON.stringify(a)))
|
||||||
|
.join(" ")}`;
|
||||||
|
stream.write(line + "\n");
|
||||||
|
if (level === "ERROR") originalError(...args);
|
||||||
|
else if (level === "WARN") originalWarn(...args);
|
||||||
|
else if (level === "INFO") originalInfo(...args);
|
||||||
|
else originalLog(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log = (...a: unknown[]) => write("LOG", a);
|
||||||
|
console.info = (...a: unknown[]) => write("INFO", a);
|
||||||
|
console.warn = (...a: unknown[]) => write("WARN", a);
|
||||||
|
console.error = (...a: unknown[]) => write("ERROR", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run the REPL loop (default single-process mode). */
|
||||||
|
async function runRepl(): Promise<void> {
|
||||||
|
const runtime = await runSingleProcess();
|
||||||
|
console.info("zesdex ready — single-process REPL mode. Type `exit` to quit.");
|
||||||
|
|
||||||
|
// Minimal REPL over stdin.
|
||||||
|
// Use node readline.
|
||||||
|
const readline = (await import("node:readline")).default;
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
rl.on("line", async (line: string) => {
|
||||||
|
const text = line.trim();
|
||||||
|
if (text === "exit" || text === "quit" || text === "/exit") {
|
||||||
|
rl.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (text === "" || text.startsWith("/")) {
|
||||||
|
if (text === "/help") {
|
||||||
|
console.log("Commands: type a message to talk to the agent; `exit` to quit.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const events: import("@zesdex/domain").TurnEvent[] = [];
|
||||||
|
const abort = new AbortController();
|
||||||
|
const params = buildTurnParamsFrom(runtime, text, events, abort);
|
||||||
|
|
||||||
|
process.stdout.write("…");
|
||||||
|
try {
|
||||||
|
await runtime.turnService.runTurn(params);
|
||||||
|
// Print the final assistant content from events.
|
||||||
|
const lastTokens: string[] = [];
|
||||||
|
for (const ev of events) {
|
||||||
|
if (ev.kind === "stream_token") lastTokens.push(ev.content);
|
||||||
|
}
|
||||||
|
const full = lastTokens.join("");
|
||||||
|
process.stdout.write("\n");
|
||||||
|
if (full.trim() !== "") console.log(full.trim());
|
||||||
|
else console.log("(no response content)");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`turn failed: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
rl.prompt();
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.prompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imported lazily to avoid a circular type-only dependency.
|
||||||
|
import { buildTurnParams } from "./compose.ts";
|
||||||
|
function buildTurnParamsFrom(
|
||||||
|
runtime: Parameters<typeof buildTurnParams>[0],
|
||||||
|
text: string,
|
||||||
|
events: import("@zesdex/domain").TurnEvent[],
|
||||||
|
abort: AbortController,
|
||||||
|
) {
|
||||||
|
return buildTurnParams(runtime, text, events, abort);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Main entry. */
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
initLogging();
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = parseCli(args);
|
||||||
|
} catch (e) {
|
||||||
|
console.error((e as Error).message);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("help" in parsed) {
|
||||||
|
console.log(parsed.text);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = parsed;
|
||||||
|
if (modeCount(opts) > 1) {
|
||||||
|
console.error(
|
||||||
|
"Cannot specify multiple modes: --daemon, --attach, --api, --ws, --grpc, --web are mutually exclusive",
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.daemon) {
|
||||||
|
console.info("daemon mode not yet wired (Fase 5) — falling back to REPL");
|
||||||
|
await runRepl();
|
||||||
|
} else if (opts.attach !== null) {
|
||||||
|
console.info(`attach mode for session '${opts.attach}' not yet wired (Fase 5)`);
|
||||||
|
process.exit(1);
|
||||||
|
} else if (opts.api) {
|
||||||
|
console.info(`api mode port ${opts.apiPort} not yet wired (Fase 5)`);
|
||||||
|
process.exit(1);
|
||||||
|
} else if (opts.ws) {
|
||||||
|
console.info(`ws mode port ${opts.wsPort} not yet wired (Fase 5)`);
|
||||||
|
process.exit(1);
|
||||||
|
} else if (opts.grpc) {
|
||||||
|
console.info(`grpc mode port ${opts.grpcPort} not yet wired (Fase 5)`);
|
||||||
|
process.exit(1);
|
||||||
|
} else if (opts.web) {
|
||||||
|
console.info(`web mode port ${opts.webPort} not yet wired (Fase 5)`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
// Default: single-process REPL.
|
||||||
|
await runRepl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user