fix: make cli check lightweight by default
This commit is contained in:
+3
-3
@@ -4,7 +4,7 @@ import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStac
|
||||
import { parseE2ERunOptions, runE2E } from "./src/e2e";
|
||||
import { emitError, emitJson } from "./src/output";
|
||||
import { jobWithTail, listJobs, readJob, runJob } from "./src/jobs";
|
||||
import { runChecks } from "./src/check";
|
||||
import { parseCheckOptions, runChecks } from "./src/check";
|
||||
import { runSsh } from "./src/ssh";
|
||||
import { extractRemoteCliOptions, runRemoteCli } from "./src/remote";
|
||||
import { runMicroserviceCommand } from "./src/microservices";
|
||||
@@ -27,7 +27,7 @@ function help(): unknown {
|
||||
{ command: "help", description: "List supported commands." },
|
||||
{ command: "--main-server-ip <ip> <command>", description: "Run selected commands through the public frontend API; use --main-server-key only for legacy SSH transport." },
|
||||
{ command: "config show", description: "Validate and print config.json as the single source of truth." },
|
||||
{ command: "check", description: "Run config, TypeScript, file presence, and docker-compose config checks." },
|
||||
{ command: "check [--full|--files|--scripts-typecheck|--components|--compose|--logs]", description: "Run the lightweight default syntax/config gate; opt into file, type, Compose, or policy checks explicitly." },
|
||||
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, provider gateway, and managed main-server user services." },
|
||||
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
|
||||
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
|
||||
@@ -151,7 +151,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (top === "check") {
|
||||
const result = runChecks(config);
|
||||
const result = runChecks(config, parseCheckOptions(args.slice(1)));
|
||||
emitJson(commandName, result, result.ok);
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
return;
|
||||
|
||||
+162
-32
@@ -1,4 +1,5 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||
import { composeConfig } from "./docker";
|
||||
@@ -9,25 +10,125 @@ interface CheckItem {
|
||||
detail: unknown;
|
||||
}
|
||||
|
||||
const syntaxFiles = [
|
||||
"scripts/cli.ts",
|
||||
"scripts/src/check.ts",
|
||||
"scripts/src/code-queue.ts",
|
||||
"scripts/src/command.ts",
|
||||
"scripts/src/decision-center.ts",
|
||||
"scripts/src/deploy.ts",
|
||||
"scripts/src/docker.ts",
|
||||
"scripts/src/e2e.ts",
|
||||
"scripts/src/remote.ts",
|
||||
"src/components/backend-core/src/index.ts",
|
||||
"src/components/frontend/src/index.ts",
|
||||
"src/components/frontend/src/app.tsx",
|
||||
"src/components/frontend/src/decision-center.tsx",
|
||||
"src/components/provider-gateway/src/index.ts",
|
||||
"src/components/microservices/oa-event-flow/src/index.ts",
|
||||
"src/components/microservices/k3sctl-adapter/src/index.ts",
|
||||
"src/components/microservices/mdtodo/src/index.ts",
|
||||
"src/components/microservices/decision-center/src/index.ts",
|
||||
];
|
||||
|
||||
export interface CheckOptions {
|
||||
full: boolean;
|
||||
files: boolean;
|
||||
scriptsTypecheck: boolean;
|
||||
components: boolean;
|
||||
compose: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
const defaultCheckOptions: CheckOptions = {
|
||||
full: false,
|
||||
files: false,
|
||||
scriptsTypecheck: false,
|
||||
components: false,
|
||||
compose: false,
|
||||
logs: false,
|
||||
};
|
||||
|
||||
export function parseCheckOptions(args: string[]): CheckOptions {
|
||||
const options = { ...defaultCheckOptions };
|
||||
for (const arg of args) {
|
||||
if (arg === "--full") {
|
||||
options.full = true;
|
||||
options.files = true;
|
||||
options.scriptsTypecheck = true;
|
||||
options.components = true;
|
||||
options.compose = true;
|
||||
options.logs = true;
|
||||
} else if (arg === "--files") {
|
||||
options.files = true;
|
||||
} else if (arg === "--scripts-typecheck") {
|
||||
options.scriptsTypecheck = true;
|
||||
} else if (arg === "--components") {
|
||||
options.components = true;
|
||||
} else if (arg === "--compose") {
|
||||
options.compose = true;
|
||||
} else if (arg === "--logs") {
|
||||
options.logs = true;
|
||||
} else if (arg === "--basic" || arg === "--syntax-only") {
|
||||
options.full = false;
|
||||
options.files = false;
|
||||
options.scriptsTypecheck = false;
|
||||
options.components = false;
|
||||
options.compose = false;
|
||||
options.logs = false;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
throw new Error("check usage: bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--components|--compose|--logs]");
|
||||
} else {
|
||||
throw new Error(`unknown check option: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function fileItem(path: string): CheckItem {
|
||||
const absolute = rootPath(path);
|
||||
return { name: `file:${path}`, ok: existsSync(absolute), detail: absolute };
|
||||
}
|
||||
|
||||
function commandItem(name: string, command: string[]): CheckItem {
|
||||
const result = runCommand(command, repoRoot);
|
||||
function commandItem(name: string, command: string[], timeoutMs = 30_000): CheckItem {
|
||||
const result = runCommand(command, repoRoot, { timeoutMs });
|
||||
return {
|
||||
name,
|
||||
ok: result.exitCode === 0,
|
||||
detail: {
|
||||
command,
|
||||
exitCode: result.exitCode,
|
||||
signal: result.signal,
|
||||
timedOut: result.timedOut,
|
||||
stdoutTail: result.stdout.slice(-4000),
|
||||
stderrTail: result.stderr.slice(-4000),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function syntaxItem(): CheckItem {
|
||||
const failures: Array<{ path: string; error: string }> = [];
|
||||
const checked: string[] = [];
|
||||
const ts = new Bun.Transpiler({ loader: "ts" });
|
||||
const tsx = new Bun.Transpiler({ loader: "tsx" });
|
||||
for (const path of syntaxFiles) {
|
||||
const absolute = rootPath(path);
|
||||
try {
|
||||
const source = readFileSync(absolute, "utf8");
|
||||
const loader = extname(path) === ".tsx" ? tsx : ts;
|
||||
loader.transformSync(source);
|
||||
checked.push(path);
|
||||
} catch (error) {
|
||||
failures.push({ path, error: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: "syntax:transpile",
|
||||
ok: failures.length === 0,
|
||||
detail: { checked, failures },
|
||||
};
|
||||
}
|
||||
|
||||
function unifiedLogRotationItem(): CheckItem {
|
||||
const serviceFiles = [
|
||||
"src/components/backend-core/src/logger.ts",
|
||||
@@ -58,38 +159,67 @@ function unifiedLogRotationItem(): CheckItem {
|
||||
};
|
||||
}
|
||||
|
||||
export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckItem[] } {
|
||||
function skippedItem(name: string, reason: string, enableWith: string): CheckItem {
|
||||
return { name, ok: true, detail: { skipped: true, reason, enableWith } };
|
||||
}
|
||||
|
||||
export function runChecks(config: UniDeskConfig, options: CheckOptions = defaultCheckOptions): { ok: boolean; mode: string; options: CheckOptions; items: CheckItem[] } {
|
||||
const items: CheckItem[] = [
|
||||
{ name: "config:validated", ok: true, detail: { project: config.project.name, runtime: config.runtime } },
|
||||
fileItem("scripts/cli.ts"),
|
||||
fileItem("AGENTS.md"),
|
||||
fileItem("TEST.md"),
|
||||
fileItem("docker-compose.yml"),
|
||||
fileItem("src/components/backend-core/src/index.ts"),
|
||||
fileItem("src/components/frontend/src/index.ts"),
|
||||
fileItem("src/components/provider-gateway/src/index.ts"),
|
||||
fileItem("src/components/microservices/oa-event-flow/src/index.ts"),
|
||||
fileItem("src/components/microservices/k3sctl-adapter/src/index.ts"),
|
||||
fileItem("src/components/microservices/mdtodo/src/index.ts"),
|
||||
fileItem("src/components/microservices/decision-center/src/index.ts"),
|
||||
fileItem("scripts/src/deploy.ts"),
|
||||
fileItem("scripts/src/e2e.ts"),
|
||||
unifiedLogRotationItem(),
|
||||
commandItem("bun:version", ["bun", "--version"]),
|
||||
commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"]),
|
||||
commandItem("typescript:components", ["bunx", "tsc", "-p", "src/tsconfig.check.json", "--pretty", "false"]),
|
||||
syntaxItem(),
|
||||
];
|
||||
const compose = composeConfig(config);
|
||||
items.push({
|
||||
name: "docker-compose:config",
|
||||
ok: compose.result.exitCode === 0,
|
||||
detail: {
|
||||
command: compose.command,
|
||||
exitCode: compose.result.exitCode,
|
||||
stdoutTail: compose.result.stdout.slice(-4000),
|
||||
stderrTail: compose.result.stderr.slice(-4000),
|
||||
runtimeEnv: compose.runtimeEnv,
|
||||
},
|
||||
});
|
||||
return { ok: items.every((item) => item.ok), items };
|
||||
if (options.files) {
|
||||
items.push(
|
||||
fileItem("scripts/cli.ts"),
|
||||
fileItem("AGENTS.md"),
|
||||
fileItem("TEST.md"),
|
||||
fileItem("docker-compose.yml"),
|
||||
fileItem("src/components/backend-core/src/index.ts"),
|
||||
fileItem("src/components/frontend/src/index.ts"),
|
||||
fileItem("src/components/provider-gateway/src/index.ts"),
|
||||
fileItem("src/components/microservices/oa-event-flow/src/index.ts"),
|
||||
fileItem("src/components/microservices/k3sctl-adapter/src/index.ts"),
|
||||
fileItem("src/components/microservices/mdtodo/src/index.ts"),
|
||||
fileItem("src/components/microservices/decision-center/src/index.ts"),
|
||||
fileItem("scripts/src/deploy.ts"),
|
||||
fileItem("scripts/src/e2e.ts"),
|
||||
);
|
||||
} else {
|
||||
items.push(skippedItem("files:required-entrypoints", "required file presence scan is opt-in", "--files or --full"));
|
||||
}
|
||||
if (options.scriptsTypecheck) {
|
||||
items.push(commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"]));
|
||||
} else {
|
||||
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
|
||||
}
|
||||
if (options.logs) {
|
||||
items.push(unifiedLogRotationItem());
|
||||
} else {
|
||||
items.push(skippedItem("logs:unified-hourly-rotation", "heavy policy scan is opt-in", "--logs or --full"));
|
||||
}
|
||||
if (options.components) {
|
||||
items.push(commandItem("typescript:components", ["bunx", "tsc", "-p", "src/tsconfig.check.json", "--pretty", "false"], 180_000));
|
||||
} else {
|
||||
items.push(skippedItem("typescript:components", "full component TypeScript check is opt-in", "--components or --full"));
|
||||
}
|
||||
if (options.compose) {
|
||||
const compose = composeConfig(config);
|
||||
items.push({
|
||||
name: "docker-compose:config",
|
||||
ok: compose.result.exitCode === 0,
|
||||
detail: {
|
||||
command: compose.command,
|
||||
exitCode: compose.result.exitCode,
|
||||
signal: compose.result.signal,
|
||||
timedOut: compose.result.timedOut,
|
||||
stdoutTail: compose.result.stdout.slice(-4000),
|
||||
stderrTail: compose.result.stderr.slice(-4000),
|
||||
runtimeEnv: compose.runtimeEnv,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push(skippedItem("docker-compose:config", "Docker Compose config rendering is opt-in", "--compose or --full"));
|
||||
}
|
||||
return { ok: items.every((item) => item.ok), mode: options.full ? "full" : "basic", options, items };
|
||||
}
|
||||
|
||||
@@ -7,20 +7,26 @@ export interface CommandResult {
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
signal: NodeJS.Signals | null;
|
||||
timedOut: boolean;
|
||||
}
|
||||
|
||||
export function runCommand(command: string[], cwd: string): CommandResult {
|
||||
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number } = {}): CommandResult {
|
||||
const result = spawnSync(command[0], command.slice(1), {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 1024 * 1024 * 8,
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
const error = result.error as (Error & { code?: string }) | undefined;
|
||||
return {
|
||||
command,
|
||||
cwd,
|
||||
exitCode: result.status,
|
||||
stdout: result.stdout ?? "",
|
||||
stderr: result.stderr ?? result.error?.message ?? "",
|
||||
stderr: result.stderr ?? error?.message ?? "",
|
||||
signal: result.signal,
|
||||
timedOut: error?.code === "ETIMEDOUT",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user