diff --git a/AGENTS.md b/AGENTS.md index 7173b515..ee045830 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `bun scripts/cli.ts help`:输出所有可用命令的 JSON 索引,详细规范见 `docs/reference/cli.md`。 - `bun scripts/cli.ts --main-server-ip `:默认通过公网 frontend 登录态远程执行调试、用户服务(底层命令名 `microservice`)、Code Queue 查询与节点自测命令,不要求主 server SSH key,详细规范见 `docs/reference/cli.md`。 - `bun scripts/cli.ts config show`:校验并展示根目录 `config.json`,配置来源规则见 `docs/reference/config.md`。 -- `bun scripts/cli.ts check`:运行配置、TypeScript、文件存在性和 Docker Compose 配置检查,测试入口见 `TEST.md`。 +- `bun scripts/cli.ts check [--full|--files|--scripts-typecheck|--components|--compose|--logs]`:默认只运行轻量配置和 TypeScript 语法检查;关键文件、`scripts/` 类型、组件类型、Docker Compose 和日志策略检查需显式开启,测试入口见 `TEST.md`。 - `bun scripts/cli.ts server start`:以异步 job 启动 database、backend-core、frontend、provider-gateway 和主 server 用户服务,部署规则见 `docs/reference/deployment.md`。 - `bun scripts/cli.ts server status`:查询固定端口、容器状态、健康检查和访问 URL,判定标准见 `docs/reference/deployment.md`。 - `bun scripts/cli.ts server logs`:分页返回文件日志与 Docker 日志尾部,日志规则见 `docs/reference/observability.md`。 diff --git a/TEST.md b/TEST.md index 36c787fa..56cb37cb 100644 --- a/TEST.md +++ b/TEST.md @@ -2,7 +2,7 @@ ## T1 CLI 可观测性与配置校验 -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts help`、`bun scripts/cli.ts config show`、`bun scripts/cli.ts check`,确认每条命令都有 JSON 输出、失败时包含错误对象、`config.json` 是唯一配置来源,且 TypeScript 检查覆盖 `scripts/` 与 `src/components/`。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts help`、`bun scripts/cli.ts config show`、`bun scripts/cli.ts check`,确认每条命令都有 JSON 输出、失败时包含错误对象、`config.json` 是唯一配置来源,且默认 `check` 只执行轻量配置和 TypeScript 语法检查;需要覆盖关键文件、`scripts/` 类型、`src/components/` 类型、Docker Compose config 和日志策略时,显式运行 `bun scripts/cli.ts check --full`。 ## T2 Docker 栈异步启动 @@ -50,7 +50,7 @@ ## T12 前端 TypeScript + React 源码约束 -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `find src/components/frontend -type f \\( -name '*.js' -o -name '*.jsx' \\) -print`,确认没有手写 frontend JS/JSX 源码;确认 `src/components/frontend/src/app.tsx` 只承担 shell/router,Todo Note、FindJob、Pipeline、MET Nonlinear、ClaudeQQ、Code Queue 分别在 `src/components/frontend/src/todo-note.tsx`、`src/components/frontend/src/findjob.tsx`、`src/components/frontend/src/pipeline.tsx`、`src/components/frontend/src/met-nonlinear.tsx`、`src/components/frontend/src/claudeqq.tsx`、`src/components/frontend/src/code-queue.tsx` 中维护;运行 `bun scripts/cli.ts check`,确认这些 TSX 模块全部纳入 TypeScript 检查,且浏览器请求 `/app.js` 由 frontend Bun server 从 TSX imports 转译生成。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `find src/components/frontend -type f \\( -name '*.js' -o -name '*.jsx' \\) -print`,确认没有手写 frontend JS/JSX 源码;确认 `src/components/frontend/src/app.tsx` 只承担 shell/router,Todo Note、FindJob、Pipeline、MET Nonlinear、ClaudeQQ、Code Queue 分别在 `src/components/frontend/src/todo-note.tsx`、`src/components/frontend/src/findjob.tsx`、`src/components/frontend/src/pipeline.tsx`、`src/components/frontend/src/met-nonlinear.tsx`、`src/components/frontend/src/claudeqq.tsx`、`src/components/frontend/src/code-queue.tsx` 中维护;运行 `bun scripts/cli.ts check --components`,确认这些 TSX 模块全部纳入 TypeScript 检查,且浏览器请求 `/app.js` 由 frontend Bun server 从 TSX imports 转译生成。 ## T13 资源节点任务管理器曲线 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0f9e9173..50fe247b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -7,7 +7,7 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定 - `help` 输出命令索引,适合作为交互式入口。 - `--main-server-ip ` 默认通过公网 frontend 登录态调用主 server 的同源 API 代理,不要求计算节点持有主 server SSH key;显式提供 `--main-server-key` 或 `--main-server-transport ssh` 时才使用旧 SSH 传输。 - `config show` 读取并校验根目录 `config.json`,不从环境变量、默认值或隐藏文件静默补配置。 -- `check` 执行配置校验、文件存在性检查、`scripts/` TypeScript 检查、`src/components/` TypeScript 检查和 Docker Compose 配置检查。 +- `check` 默认只执行轻量配置校验、Bun 版本检查和 Bun Transpiler 语法解析(覆盖 CLI 入口、主要 `scripts/` 模块和核心组件入口,不做类型推导);关键文件存在性、`scripts/` TypeScript 类型检查、`src/components/` TypeScript 类型检查、Docker Compose config 和日志轮转策略扫描默认不启用,分别通过 `--files`、`--scripts-typecheck`、`--components`、`--compose`、`--logs` 开启,或用 `--full` 一次性开启。 - `server start` 创建异步 job,在后台执行 Docker 构建和启动;命令本身只负责返回 job id、日志路径和启动命令。 - `server stop` 创建异步 job,在后台停止固定 Compose project 中的全部 UniDesk 服务。 - `server status` 查询公开端口、受限宿主端口、内部端口、Compose 容器、core/frontend/provider/database 健康检查和访问 URL;D601 Code Queue 使用的 PostgreSQL/OA Event Flow host mapping 必须出现在受限宿主端口而不是无条件公开入口中。 diff --git a/scripts/cli.ts b/scripts/cli.ts index 8bb7e673..ac047901 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -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 ", 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 { } 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; diff --git a/scripts/src/check.ts b/scripts/src/check.ts index 57c463e5..c7d5fba6 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -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 }; } diff --git a/scripts/src/command.ts b/scripts/src/command.ts index 468992dd..6c58c153 100644 --- a/scripts/src/command.ts +++ b/scripts/src/command.ts @@ -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", }; }