fix(build): prepare CLI and TUI build output with Node instead of POSIX tools (originally #956 by @dajiaohuang) (#1033)

The CLI and TUI builds prepare their output with Node instead of POSIX shell tools, so building works in a plain Windows shell. Originally contributed by @dajiaohuang in #956.
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-08 12:29:43 -07:00
1 parent dc234f0de0
commit c502584b18
5 files changed
+126 -2

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@
"scripts"
],
"scripts": {
"build": "tsc && chmod +x dist/bin-wrapper.js && mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/ && mkdir -p dist/lib/scope-templates && cp src/lib/scope-templates/*.md dist/lib/scope-templates/ && cp ../../LICENSE LICENSE && cp ../../README.md README.md",
"build": "tsc && node ../../scripts/prepare-workspace-build.mjs cli",
"postinstall": "node scripts/check-abi.mjs",
"prepublishOnly": "bash ../../scripts/build-package.sh",
"test": "vitest run",
+1 -1
View File
@@ -9,7 +9,7 @@
"openrig-tui": "dist/main.js"
},
"scripts": {
"build": "tsc && chmod +x dist/main.js",
"build": "tsc && node ../../scripts/prepare-workspace-build.mjs tui",
"pretest": "npm run build",
"test": "vitest run",
"test:watch": "vitest"
+2
View File
@@ -14,6 +14,8 @@ test("the CLI build carries the unchanged repository LICENSE and README into npm
copyFileSync("LICENSE", join(root, "LICENSE"));
copyFileSync("README.md", join(root, "README.md"));
copyFileSync("packages/cli/package.json", join(cli, "package.json"));
mkdirSync(join(root, "scripts"), { recursive: true });
copyFileSync("scripts/prepare-workspace-build.mjs", join(root, "scripts/prepare-workspace-build.mjs"));
writeFileSync(join(cli, "src/schemas/fixture.json"), "{}\n");
writeFileSync(join(cli, "src/lib/scope-templates/fixture.md"), "fixture\n");
// Compilation is outside this packaging regression. Run the actual build
+34
View File
@@ -0,0 +1,34 @@
import { chmodSync, copyFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
/** Finish a compiled workspace with the same assets/modes on every host. */
export function prepareWorkspaceBuild(root, workspace, platform = process.platform) {
if (workspace !== "cli" && workspace !== "tui") throw new Error(`Unknown workspace: ${workspace}`);
const dir = path.join(root, "packages", workspace);
const entry = path.join(dir, "dist", workspace === "cli" ? "bin-wrapper.js" : "main.js");
const mode = statSync(entry).mode;
if (platform !== "win32") chmodSync(entry, mode | 0o111);
if (workspace === "tui") return;
for (const [source, target, extension] of [
["src/schemas", "dist/schemas", ".json"],
["src/lib/scope-templates", "dist/lib/scope-templates", ".md"],
]) {
const destination = path.join(dir, target);
mkdirSync(destination, { recursive: true });
let copied = 0;
for (const item of readdirSync(path.join(dir, source), { withFileTypes: true })) {
if (item.name.startsWith(".") || !item.name.endsWith(extension)) continue;
const file = path.join(dir, source, item.name);
if (item.isFile() || (item.isSymbolicLink() && statSync(file).isFile())) {
copyFileSync(file, path.join(destination, item.name));
copied++;
}
}
if (!copied) throw new Error(`No ${extension} files matched in ${source}`);
}
for (const name of ["LICENSE", "README.md"]) copyFileSync(path.join(root, name), path.join(dir, name));
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href)
prepareWorkspaceBuild(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), process.argv[2]);
+88
View File
@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, statSync, rmSync, symlinkSync, existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { prepareWorkspaceBuild } from "./prepare-workspace-build.mjs";
test("CLI build copies assets and root documents without shell utilities", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "openrig-build-assets-"));
try {
for (const dir of ["packages/cli/dist", "packages/cli/src/schemas", "packages/cli/src/lib/scope-templates"])
mkdirSync(path.join(root, dir), { recursive: true });
const files = {
"LICENSE": "license", "README.md": "readme", "packages/cli/dist/bin-wrapper.js": "entry",
"packages/cli/src/schemas/spec.json": "{}", "packages/cli/src/schemas/ignored.txt": "ignore",
"packages/cli/src/lib/scope-templates/team.md": "team",
"packages/cli/src/schemas/.hidden.json": "hidden schema",
"packages/cli/src/lib/scope-templates/.hidden.md": "hidden template",
};
for (const [name, content] of Object.entries(files)) writeFileSync(path.join(root, name), content);
prepareWorkspaceBuild(root, "cli", process.platform);
for (const [name, content] of [["dist/schemas/spec.json", "{}"], ["dist/lib/scope-templates/team.md", "team"], ["LICENSE", "license"], ["README.md", "readme"]])
assert.equal(readFileSync(path.join(root, "packages/cli", name), "utf8"), content);
assert.throws(() => readFileSync(path.join(root, "packages/cli/dist/schemas/ignored.txt")));
assert.equal(existsSync(path.join(root, "packages/cli/dist/schemas/.hidden.json")), false);
assert.equal(existsSync(path.join(root, "packages/cli/dist/lib/scope-templates/.hidden.md")), false);
if (process.platform !== "win32") assert.equal(statSync(path.join(root, "packages/cli/dist/bin-wrapper.js")).mode & 0o111, 0o111);
writeFileSync(path.join(root, "packages/cli/src/schemas/spec.json"), "updated");
prepareWorkspaceBuild(root, "cli", process.platform);
assert.equal(readFileSync(path.join(root, "packages/cli/dist/schemas/spec.json"), "utf8"), "updated");
} finally { rmSync(root, { recursive: true, force: true }); }
});
function cliFixture() {
const root = mkdtempSync(path.join(os.tmpdir(), "openrig-build-selection-"));
for (const dir of ["packages/cli/dist", "packages/cli/src/schemas", "packages/cli/src/lib/scope-templates"])
mkdirSync(path.join(root, dir), { recursive: true });
for (const name of ["LICENSE", "README.md", "packages/cli/dist/bin-wrapper.js"])
writeFileSync(path.join(root, name), "fixture");
return root;
}
test("CLI preparation follows file symlinks like the old cp glob", t => {
const root = cliFixture();
try {
writeFileSync(path.join(root, "schema-source.txt"), "linked schema");
writeFileSync(path.join(root, "template-source.txt"), "linked template");
try {
symlinkSync(path.join(root, "schema-source.txt"), path.join(root, "packages/cli/src/schemas/linked.json"), "file");
symlinkSync(path.join(root, "template-source.txt"), path.join(root, "packages/cli/src/lib/scope-templates/linked.md"), "file");
} catch (error) {
if (process.platform === "win32" && ["EPERM", "EACCES"].includes(error.code)) {
t.skip("Windows file-symlink privilege unavailable; requires POSIX validation"); return;
}
throw error;
}
prepareWorkspaceBuild(root, "cli");
assert.equal(readFileSync(path.join(root, "packages/cli/dist/schemas/linked.json"), "utf8"), "linked schema");
assert.equal(readFileSync(path.join(root, "packages/cli/dist/lib/scope-templates/linked.md"), "utf8"), "linked template");
assert.equal(statSync(path.join(root, "packages/cli/dist/schemas/linked.json")).isFile(), true);
} finally { rmSync(root, { recursive: true, force: true }); }
});
test("CLI preparation fails when a source glob would have no matches", () => {
for (const [source, other, extension] of [
["src/schemas", "src/lib/scope-templates/team.md", ".json"],
["src/lib/scope-templates", "src/schemas/spec.json", ".md"],
]) {
const root = cliFixture();
try {
writeFileSync(path.join(root, "packages/cli", other), "fixture");
writeFileSync(path.join(root, "packages/cli", source, `.hidden${extension}`), "not a glob match");
assert.throws(() => prepareWorkspaceBuild(root, "cli"), /No .* files matched/);
} finally { rmSync(root, { recursive: true, force: true }); }
}
});
test("TUI preparation requires the compiled entry and rejects unknown workspaces", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "openrig-build-tui-"));
try {
assert.throws(() => prepareWorkspaceBuild(root, "tui"), /ENOENT/);
mkdirSync(path.join(root, "packages/tui/dist"), { recursive: true });
writeFileSync(path.join(root, "packages/tui/dist/main.js"), "entry");
prepareWorkspaceBuild(root, "tui", process.platform);
if (process.platform !== "win32") assert.equal(statSync(path.join(root, "packages/tui/dist/main.js")).mode & 0o111, 0o111);
assert.throws(() => prepareWorkspaceBuild(root, "other"), /Unknown workspace/);
} finally { rmSync(root, { recursive: true, force: true }); }
});