mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
The WebSocket message-type schema is the one artifact in this repository that neither component owns: `Server/ws/message_types.go` and `Client/src/lib/protocolTypes.ts` are both generated from it, and neither may be hand-edited. It nonetheless lived at `docs/protocol-schema.json` — filed under the directory for prose, whose own README calls it "Reference" material — and its generator lived at `Server/scripts/genprotocol/`, i.e. inside one of the two consumers. Ownership was legible from neither location. The obvious fix — move the generator to the repository root alongside the schema, so the whole tool is at the cross-component boundary — is wrong here. The generator is a Go `package main`, and Go modules are directory-rooted: `Server/go.mod` roots at `Server/`, so a root-level Go program needs a second module or a `go.work`. That second module would sit outside every path filter this repository already has — `golangci-lint` runs with `working-directory: Server/` (ci.yml), `go vet ./...` runs from `Server/` (scripts/run.mjs, .githooks/pre-commit), `.githooks/pre-commit` selects Go files with `^Server/.*\.go$`, `.githooks/pre-push` sets `server_changed` on `^Server/`, setup-go caches on `Server/go.sum`, and dependabot has one gomod block for `/Server`. Six gates would silently stop covering the generator, each failing open. The schema is data and moves freely; the generator is Go and stays where the Go toolchain already runs. Done instead: - `docs/protocol-schema.json` -> `protocol/schema.json`. A new top-level `protocol/` is the cross-component boundary, with a `README.md` naming the two generated consumers, the one command, and the four gates. - `Server/scripts/genprotocol/` -> `Server/cmd/genprotocol/`, the module's conventional home for an executable. This also empties `Server/scripts/` of Go entry points except `seed.go`, which RL-10 moves next. - `Server/cmd/` added to `Server/.dockerignore` and `Server/.air.toml`, which both already excluded `Server/scripts/`. Without this the move would have silently widened the Docker build context and the air watch set. 27 files, 115 insertions, 76 deletions. Two runtime path resolvers re-pointed (`cmd/genprotocol/main.go:41` `-schema` default, `ws/protocol_contract_test.go:67` `filepath.Join`); two git-hook grep patterns (`pre-commit:53`, `pre-push:57`); eight generator call sites across five files (Makefile x2, scripts/run.mjs x2, pre-commit x2, ci-check skill, bughunt-fix.js); two broken relative markdown links (docs/README.md:47, docs/protocol.md:1497); two generated files regenerated, header lines only, zero constants changed; two ledger prose hits plus a `render-ledger.mjs` re-render. No new verify was written: the regenerate-and-diff check is already enforced three times (CI `make protocol-verify`, `.githooks/pre-commit`, `npm run check:server`) and `ws/protocol_contract_test.go` independently checks the schema against the constants a fourth time. Verified: both directions, for both resolvers. With `protocol/schema.json` removed, `go test ./ws/ -run TestProtocol` fails with `reading protocol schema at /home/user/OwnCord/protocol/schema.json: no such file or directory` (two tests) and `go run ./cmd/genprotocol` exits 1 with `read schema: open ../protocol/schema.json: no such file or directory`; with the file restored both pass. So the new path is genuinely resolved, not merely spelled in a comment. The hook patterns were exercised directly: the pre-commit pattern matches `protocol/schema.json` and `Server/cmd/genprotocol/main.go` and no longer matches `docs/protocol-schema.json`; the pre-push pattern matches `protocol/schema.json`. `go run ./cmd/genprotocol` twice in a row leaves `git diff --exit-code ws/message_types.go ../Client/src/lib/protocolTypes.ts` clean, so the committed outputs are exactly what the generator emits. `go build ./...` and `go vet ./...` pass; `npx prettier --check .`, `npm run typecheck` and `npm run lint` pass; `node .superpowers/render-ledger.mjs --check` reports 348 findings valid. Not included: the four dated `docs/audit-*.md` files, the older `docs/plans/*`, and `CHANGELOG.md` keep the old path — they are point-in-time records, and `.prettierignore` and `scripts/check-doc-counts.mjs` already treat them as deliberately unmaintained. The B1 plan itself keeps its own wording, since it states intent rather than current state. `Server/scripts/` is not deleted: it still holds `seed.go` (RL-10), `k6/`, `toxiproxy/` and two shell scripts. `Server/telemetry/metrics.go:19` declares a scope for a `Server/voice` package that does not exist — spotted here, unrelated to this move, left for RL-13's sweep to carry forward verbatim rather than fixed inside a relocation. No `seed:` Make target was added. Refs RL-09, L-09
259 lines
9.3 KiB
JavaScript
259 lines
9.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Root command facade (RL-04 / L-04).
|
|
//
|
|
// One entry point for the checks CI runs, so a contributor does not have to
|
|
// know which directory each stack lives in. `node scripts/run.mjs --list`
|
|
// prints every task and the exact commands it runs.
|
|
//
|
|
// Two rules this file exists to keep:
|
|
//
|
|
// 1. Cross-platform. No `make`, no shell syntax, no `cd &&`. Every step is
|
|
// spawned directly with an explicit `cwd`, so there is no shell to quote
|
|
// for and nothing that behaves differently on Windows.
|
|
// 2. The facade orchestrates, it never becomes the only path. Each step
|
|
// prints the command it runs, in the directory it runs it in, so a
|
|
// Go-only contributor can read the output and type those commands
|
|
// instead — and never needs Node to work on the server.
|
|
//
|
|
// Dependency-free by design: Node's standard library only, like
|
|
// .superpowers/render-ledger.mjs. Adding a dependency here would mean
|
|
// `npm run check` could not run until `npm install` had.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const WIN = process.platform === "win32";
|
|
|
|
// npm and npx are batch shims on Windows; everything else is a real binary.
|
|
const bin = (c) => (WIN && (c === "npm" || c === "npx") ? `${c}.cmd` : c);
|
|
|
|
// Node refuses to spawn a .cmd/.bat with shell:false (the CVE-2024-27980
|
|
// mitigation): it fails with EINVAL and a null exit status. So the Windows npm
|
|
// shims need a shell, and only they do -- every other command here is a real
|
|
// binary, and a shell would put its quoting rules between us and the arguments.
|
|
const needsShell = (c) => WIN && (c === "npm" || c === "npx");
|
|
|
|
/** A step that always runs. */
|
|
const step = (cmd, args, cwd = ".") => ({ cmd, args, cwd });
|
|
|
|
/**
|
|
* A step that is skipped, with a printed reason, when `probe` is not on PATH.
|
|
* Used for tools CI installs but a contributor may not have: golangci-lint has
|
|
* no wrapper in this repo at all, and sqlc is pinned by Server/sqlc.version.
|
|
*/
|
|
const optional = (probe, cmd, args, cwd, why) => ({ cmd, args, cwd, probe, why });
|
|
|
|
/**
|
|
* Tracked files matching `patterns`. shellcheck and actionlint take file lists,
|
|
* and the only correct list is the tracked one: `.claude/worktrees/` holds
|
|
* gitignored copies of the tree that a filesystem glob would happily lint.
|
|
*/
|
|
const tracked = (...patterns) => {
|
|
const r = spawnSync("git", ["ls-files", "-z", ...patterns], { cwd: ROOT, encoding: "utf8" });
|
|
return r.status === 0 ? r.stdout.split("\0").filter(Boolean) : [];
|
|
};
|
|
|
|
// `git diff --exit-code` after regenerating is what `make protocol-verify` and
|
|
// `make sqlc-verify` reduce to. Inlined so neither needs make.
|
|
const PROTOCOL_VERIFY = [
|
|
step("go", ["run", "./cmd/genprotocol"], "Server"),
|
|
step(
|
|
"git",
|
|
["diff", "--exit-code", "ws/message_types.go", "../Client/src/lib/protocolTypes.ts"],
|
|
"Server",
|
|
),
|
|
];
|
|
const SQLC_VERIFY = [
|
|
optional(
|
|
"sqlc",
|
|
"sqlc",
|
|
["generate"],
|
|
"Server",
|
|
"sqlc not on PATH — install the version in Server/sqlc.version",
|
|
),
|
|
optional("sqlc", "git", ["diff", "--exit-code", "db/dbgen"], "Server", "sqlc not on PATH"),
|
|
];
|
|
|
|
const CHECK_SERVER = [
|
|
step("go", ["build", "./..."], "Server"),
|
|
step("go", ["build", "-tags", "otel", "./..."], "Server"),
|
|
step("go", ["build", "-tags", "wazero", "./..."], "Server"),
|
|
step("go", ["build", "-tags", "otel,wazero", "./..."], "Server"),
|
|
step("go", ["vet", "./..."], "Server"),
|
|
step("go", ["test", "-race", "./..."], "Server"),
|
|
step("go", ["test", "-tags", "deadlock", "-count=1", "./ws/"], "Server"),
|
|
optional(
|
|
"golangci-lint",
|
|
"golangci-lint",
|
|
["run", "./..."],
|
|
"Server",
|
|
"golangci-lint not on PATH — CI pins v2.11.3",
|
|
),
|
|
...PROTOCOL_VERIFY,
|
|
...SQLC_VERIFY,
|
|
];
|
|
|
|
const CHECK_CLIENT = [
|
|
step("npm", ["run", "typecheck"], "Client"),
|
|
step("npm", ["run", "lint"], "Client"),
|
|
step("npm", ["test"], "Client"),
|
|
];
|
|
|
|
// Matches ci.yml's Rust Unit Tests job exactly: --lib for tests, --all-targets
|
|
// for clippy. They differ deliberately; do not "align" them.
|
|
const CHECK_RUST = [
|
|
step("cargo", ["fmt", "--all", "--", "--check"], "Client/src-tauri"),
|
|
step("cargo", ["test", "--lib"], "Client/src-tauri"),
|
|
step("cargo", ["clippy", "--all-targets", "--", "-D", "warnings"], "Client/src-tauri"),
|
|
];
|
|
|
|
// Fast and dependency-free, so it goes first: a contradicted count should not
|
|
// wait behind ten minutes of -race.
|
|
const CHECK_DOCS = [step("node", ["scripts/check-doc-counts.mjs"], ".")];
|
|
|
|
// Repository-wide formatting and script/workflow lint (RL-19 / L-13, S-05).
|
|
//
|
|
// No `gofmt -l` step here on purpose: `gofmt -l` prints offenders and still
|
|
// exits 0, so it cannot fail a build. Go formatting is enforced by the
|
|
// `formatters` block in Server/.golangci.yml, which runs inside the pinned
|
|
// Lint check, and by .githooks/pre-commit on staged files.
|
|
const CHECK_HYGIENE = [
|
|
step("npx", ["prettier", "--check", "."], "."),
|
|
optional(
|
|
"shellcheck",
|
|
"shellcheck",
|
|
tracked("*.sh", ".githooks/pre-commit", ".githooks/pre-push"),
|
|
".",
|
|
"shellcheck not on PATH — no clean Windows install; CI runs it",
|
|
),
|
|
optional(
|
|
"actionlint",
|
|
"actionlint",
|
|
tracked(".github/workflows/*.yml"),
|
|
".",
|
|
"actionlint not on PATH — no clean Windows install; CI runs it",
|
|
),
|
|
];
|
|
|
|
const TASKS = {
|
|
bootstrap: [
|
|
step("npm", ["ci"], "."),
|
|
step("npm", ["ci"], "Client"),
|
|
step("npm", ["ci"], "tools/mcp-introspect"),
|
|
],
|
|
"check:server": CHECK_SERVER,
|
|
"check:client": CHECK_CLIENT,
|
|
"check:rust": CHECK_RUST,
|
|
"check:docs": CHECK_DOCS,
|
|
"check:hygiene": CHECK_HYGIENE,
|
|
check: [...CHECK_DOCS, ...CHECK_HYGIENE, ...CHECK_SERVER, ...CHECK_CLIENT, ...CHECK_RUST],
|
|
generate: [
|
|
step("go", ["run", "./cmd/genprotocol"], "Server"),
|
|
optional(
|
|
"sqlc",
|
|
"sqlc",
|
|
["generate"],
|
|
"Server",
|
|
"sqlc not on PATH — install the version in Server/sqlc.version",
|
|
),
|
|
],
|
|
format: [
|
|
step("npx", ["prettier", "--write", "."], "."),
|
|
optional("gofmt", "gofmt", ["-w", "."], "Server", "gofmt not on PATH"),
|
|
step("cargo", ["fmt", "--all"], "Client/src-tauri"),
|
|
],
|
|
"release:preflight": [
|
|
...CHECK_DOCS,
|
|
...CHECK_HYGIENE,
|
|
...CHECK_SERVER,
|
|
...CHECK_CLIENT,
|
|
...CHECK_RUST,
|
|
step("npm", ["run", "build"], "Client"),
|
|
],
|
|
};
|
|
|
|
// Resolve against PATH directly instead of shelling out to `where`/`command`.
|
|
// Both probes were unreliable: `where.exe` lives in C:\WINDOWS\System32, which a
|
|
// Git Bash PATH does not always contain (it can carry only the subdirectories),
|
|
// and a probe that cannot start reports "not installed" for a tool that is. That
|
|
// turned every optional() step into a permanent SKIP on Windows.
|
|
function onPath(cmd) {
|
|
const exts = WIN ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
const dirs = (process.env.PATH || "").split(WIN ? ";" : ":");
|
|
return dirs.some((dir) => dir && exts.some((ext) => existsSync(join(dir, cmd + ext))));
|
|
}
|
|
|
|
function runTask(name) {
|
|
const steps = TASKS[name];
|
|
if (!steps) {
|
|
console.error(`unknown task: ${name}\nknown: ${Object.keys(TASKS).join(", ")}`);
|
|
process.exit(2);
|
|
}
|
|
const skipped = [];
|
|
for (const s of steps) {
|
|
if (s.probe && !onPath(s.probe)) {
|
|
console.log(`\n--- SKIP ${s.cmd} ${s.args.join(" ")} (${s.why})`);
|
|
skipped.push(s.probe);
|
|
continue;
|
|
}
|
|
const where = s.cwd === "." ? "" : ` [in ${s.cwd}]`;
|
|
console.log(`\n--- ${s.cmd} ${s.args.join(" ")}${where}`);
|
|
// With shell:true Node deprecates a separate args array (DEP0190), because it
|
|
// concatenates without escaping. So concatenate deliberately instead: the only
|
|
// commands that take this branch are the npm shims, and no argument in this
|
|
// file contains a space.
|
|
const shell = needsShell(s.cmd);
|
|
const r = shell
|
|
? spawnSync([bin(s.cmd), ...s.args].join(" "), {
|
|
cwd: join(ROOT, s.cwd),
|
|
stdio: "inherit",
|
|
shell: true,
|
|
})
|
|
: spawnSync(s.cmd, s.args, {
|
|
cwd: join(ROOT, s.cwd),
|
|
stdio: "inherit",
|
|
shell: false,
|
|
});
|
|
if (r.error && r.error.code !== "ENOENT") {
|
|
console.error(`\nFAILED: ${s.cmd} could not be started: ${r.error.code}`);
|
|
process.exit(1);
|
|
}
|
|
if (r.error && r.error.code === "ENOENT") {
|
|
console.error(`\nFAILED: ${s.cmd} is not installed or not on PATH.`);
|
|
process.exit(1);
|
|
}
|
|
if (r.status !== 0) {
|
|
console.error(`\nFAILED: ${s.cmd} ${s.args.join(" ")}${where} exited ${r.status}`);
|
|
process.exit(r.status ?? 1);
|
|
}
|
|
}
|
|
if (skipped.length) {
|
|
console.log(
|
|
`\n${name}: passed, with ${[...new Set(skipped)].join(", ")} skipped (not installed). CI runs them.`,
|
|
);
|
|
} else {
|
|
console.log(`\n${name}: passed`);
|
|
}
|
|
}
|
|
|
|
const arg = process.argv[2];
|
|
if (!arg || arg === "--list") {
|
|
for (const [name, steps] of Object.entries(TASKS)) {
|
|
console.log(`\n${name}`);
|
|
for (const s of steps) {
|
|
const where = s.cwd === "." ? "" : ` (in ${s.cwd})`;
|
|
console.log(` ${s.probe ? "[optional] " : ""}${s.cmd} ${s.args.join(" ")}${where}`);
|
|
}
|
|
}
|
|
console.log("");
|
|
process.exit(0);
|
|
}
|
|
if (!existsSync(join(ROOT, "Server")) || !existsSync(join(ROOT, "Client"))) {
|
|
console.error("run this from the repository root");
|
|
process.exit(2);
|
|
}
|
|
runTask(arg);
|