96 lines
4.8 KiB
TypeScript
96 lines
4.8 KiB
TypeScript
import { runCommand } from "./command";
|
||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
|
||
|
||
type Service = "api" | "worker" | "web";
|
||
type Action = "start" | "stop" | "restart" | "status" | "logs";
|
||
|
||
export function hwlabNativeDevelopmentHelp(): Record<string, unknown> {
|
||
return {
|
||
ok: true,
|
||
command: "hwlab nodes native-development workbench",
|
||
usage: [
|
||
"bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node <node> --lane <lane>",
|
||
],
|
||
contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的公网 IP 和 port;bind/probe 地址不作为用户入口。",
|
||
configPath: hwlabRuntimeLaneConfigPath(),
|
||
};
|
||
}
|
||
|
||
export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string, unknown> {
|
||
if (args.length === 0 || args.includes("--help") || args.includes("-h")) return hwlabNativeDevelopmentHelp();
|
||
const [application, serviceRaw, actionRaw] = args;
|
||
if (application !== "workbench") throw new Error("native-development currently supports workbench");
|
||
const service = parseService(serviceRaw);
|
||
const action = parseAction(actionRaw);
|
||
const node = requiredOption(args, "--node");
|
||
const lane = requiredOption(args, "--lane");
|
||
if (!isHwlabRuntimeLane(lane)) throw new Error(`--lane must be declared in ${hwlabRuntimeLaneConfigPath()}`);
|
||
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||
const native = spec.nativeDevelopment?.workbench;
|
||
if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench is required`);
|
||
const env = hwlabNativeDevelopmentEnvironment(native);
|
||
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", "service", service, action];
|
||
const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } });
|
||
const payload = parsePayload(result.stdout);
|
||
const publicEndpoint = service === "api"
|
||
? { ip: native.publicHost, port: native.api.port, url: `http://${native.publicHost}:${native.api.port}` }
|
||
: service === "web"
|
||
? { ip: native.publicHost, port: native.web.port, url: `http://${native.publicHost}:${native.web.port}/workbench` }
|
||
: null;
|
||
return {
|
||
ok: result.exitCode === 0 && payload.ok !== false,
|
||
command: `hwlab nodes native-development workbench ${service} ${action}`,
|
||
node,
|
||
lane,
|
||
service,
|
||
action,
|
||
configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench`,
|
||
publicHostSource: native.publicHostRef,
|
||
userEndpoint: publicEndpoint,
|
||
serviceResult: payload,
|
||
...(result.exitCode === 0 ? {} : { error: { code: "hwlab_native_service_failed", exitCode: result.exitCode, stderr: result.stderr.trim().slice(-2000) } }),
|
||
};
|
||
}
|
||
|
||
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"]): NodeJS.ProcessEnv {
|
||
return {
|
||
WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir,
|
||
WORKBENCH_API_BIND_HOST: native.api.bindHost,
|
||
WORKBENCH_API_PROBE_HOST: native.api.probeHost,
|
||
WORKBENCH_API_PUBLIC_HOST: native.publicHost,
|
||
WORKBENCH_API_PORT: String(native.api.port),
|
||
WORKBENCH_WORKER_BIND_HOST: native.worker.bindHost,
|
||
WORKBENCH_WORKER_PROBE_HOST: native.worker.probeHost,
|
||
WORKBENCH_WORKER_HEALTH_PORT: String(native.worker.healthPort),
|
||
WORKBENCH_WEB_BIND_HOST: native.web.bindHost,
|
||
WORKBENCH_WEB_PROBE_HOST: native.web.probeHost,
|
||
WORKBENCH_WEB_PUBLIC_HOST: native.publicHost,
|
||
WORKBENCH_WEB_PORT: String(native.web.port),
|
||
WORKBENCH_NATIVE_API_URL: `http://${native.api.probeHost}:${native.api.port}`,
|
||
WORKBENCH_WEB_RUNTIME_CONFIG: JSON.stringify(native.web.runtimeConfig),
|
||
};
|
||
}
|
||
|
||
function parsePayload(stdout: string): Record<string, unknown> {
|
||
const parsed = JSON.parse(stdout) as unknown;
|
||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("HWLAB Workbench CLI returned invalid JSON");
|
||
return parsed as Record<string, unknown>;
|
||
}
|
||
|
||
function parseService(value: string | undefined): Service {
|
||
if (value === "api" || value === "worker" || value === "web") return value;
|
||
throw new Error("native-development workbench service must be api, worker, or web");
|
||
}
|
||
|
||
function parseAction(value: string | undefined): Action {
|
||
if (value === "start" || value === "stop" || value === "restart" || value === "status" || value === "logs") return value;
|
||
throw new Error("native-development workbench action must be start, stop, restart, status, or logs");
|
||
}
|
||
|
||
function requiredOption(args: string[], option: string): string {
|
||
const index = args.indexOf(option);
|
||
const value = index < 0 ? undefined : args[index + 1];
|
||
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
|
||
return value;
|
||
}
|