fix(packaging): restore missing copy-dist-to-packaged script

apps/dashboard and apps/tui call scripts/copy-dist-to-packaged.mjs from
build:packaged, but the script was missing and scripts/ was gitignored.
Add the copier, Node smoke tests, and CI checks so packaged UI/TUI
artifacts can ship in wheels again.
This commit is contained in:
Ayush7614 2026-07-29 02:56:45 +05:30
parent becf819e8c
commit 4df52989fa
4 changed files with 238 additions and 2 deletions

View file

@ -29,7 +29,27 @@ jobs:
- name: Verify PyYAML is importable - name: Verify PyYAML is importable
run: python -c "import yaml; print(yaml.__version__)" run: python -c "import yaml; print(yaml.__version__)"
- name: Run test suite - name: Run Python test suite
run: > run: >
pytest tests/ -q --tb=short pytest tests/ -q --tb=short
--ignore=tests/benchmarks/terminal_bench/test_openspace_harbor_agent_config.py --ignore=tests/benchmarks/terminal_bench/test_openspace_harbor_agent_config.py
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run packaging copy-script tests
run: node --test tests/packaging/test_copy_dist_to_packaged.mjs
- name: Verify wheel can include packaged frontend paths
run: |
python - <<'PY'
from pathlib import Path
pyproject = Path("pyproject.toml").read_text(encoding="utf-8")
assert 'packaged/dashboard/**/*' in pyproject
assert 'packaged/tui/**/*' in pyproject
script = Path("scripts/copy-dist-to-packaged.mjs")
assert script.is_file(), "missing packaging copy script"
print("packaging contract ok")
PY

7
.gitignore vendored
View file

@ -68,7 +68,12 @@ tests/entrypoints/*
!tests/entrypoints/dashboard/ !tests/entrypoints/dashboard/
tests/entrypoints/dashboard/* tests/entrypoints/dashboard/*
!tests/entrypoints/dashboard/test_dashboard_auth.py !tests/entrypoints/dashboard/test_dashboard_auth.py
scripts/ !tests/packaging/
tests/packaging/*
!tests/packaging/test_copy_dist_to_packaged.mjs
scripts/*
!scripts/
!scripts/copy-dist-to-packaged.mjs
# Local agent/project memory # Local agent/project memory
OPENSPACE.md OPENSPACE.md

View file

@ -0,0 +1,131 @@
#!/usr/bin/env node
/**
* Copy a frontend build `dist/` into openspace/packaged/<target>.
*
* Usage:
* node scripts/copy-dist-to-packaged.mjs <distDir> <destDir> \
* [--node-modules <dir>] [--package-json <file>]
*
* Dashboard only needs the Vite dist tree.
* TUI also copies production node_modules + package.json so the packaged
* Ink entrypoint can run without a separate npm install after pip install.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
function usage(message) {
if (message) {
console.error(message);
}
console.error(
"Usage: node scripts/copy-dist-to-packaged.mjs <distDir> <destDir> " +
"[--node-modules <dir>] [--package-json <file>]",
);
process.exit(1);
}
function parseArgs(argv) {
const positional = [];
let nodeModules = null;
let packageJson = null;
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--node-modules") {
nodeModules = argv[++i];
if (!nodeModules) usage("Missing value for --node-modules");
continue;
}
if (arg === "--package-json") {
packageJson = argv[++i];
if (!packageJson) usage("Missing value for --package-json");
continue;
}
if (arg.startsWith("-")) {
usage(`Unknown option: ${arg}`);
}
positional.push(arg);
}
if (positional.length !== 2) {
usage("Expected exactly <distDir> and <destDir>");
}
return {
distDir: path.resolve(positional[0]),
destDir: path.resolve(positional[1]),
nodeModules: nodeModules ? path.resolve(nodeModules) : null,
packageJson: packageJson ? path.resolve(packageJson) : null,
};
}
function assertDirectory(dirPath, label) {
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
throw new Error(`${label} is missing or not a directory: ${dirPath}`);
}
}
function assertFile(filePath, label) {
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
throw new Error(`${label} is missing or not a file: ${filePath}`);
}
}
function emptyDirectory(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
for (const entry of fs.readdirSync(dirPath)) {
fs.rmSync(path.join(dirPath, entry), { recursive: true, force: true });
}
}
function copyTree(source, destination) {
fs.cpSync(source, destination, {
recursive: true,
force: true,
errorOnExist: false,
});
}
function main(argv = process.argv.slice(2)) {
const options = parseArgs(argv);
assertDirectory(options.distDir, "distDir");
if (options.nodeModules) {
assertDirectory(options.nodeModules, "nodeModules");
}
if (options.packageJson) {
assertFile(options.packageJson, "packageJson");
}
emptyDirectory(options.destDir);
copyTree(options.distDir, options.destDir);
if (options.nodeModules) {
copyTree(options.nodeModules, path.join(options.destDir, "node_modules"));
}
if (options.packageJson) {
fs.copyFileSync(
options.packageJson,
path.join(options.destDir, "package.json"),
);
}
console.log(`Packaged assets copied to ${options.destDir}`);
return options.destDir;
}
const isDirectRun =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isDirectRun) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
export { main, parseArgs };

View file

@ -0,0 +1,80 @@
/**
* Smoke tests for scripts/copy-dist-to-packaged.mjs
*/
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { main } from "../../scripts/copy-dist-to-packaged.mjs";
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../..",
);
test("copies dist tree into packaged destination", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openspace-pack-"));
const dist = path.join(tmp, "dist");
const dest = path.join(tmp, "packaged");
fs.mkdirSync(dist, { recursive: true });
fs.writeFileSync(path.join(dist, "index.html"), "<html>ok</html>\n");
fs.mkdirSync(path.join(dist, "assets"), { recursive: true });
fs.writeFileSync(path.join(dist, "assets", "app.js"), "console.log(1);\n");
main([dist, dest]);
assert.equal(
fs.readFileSync(path.join(dest, "index.html"), "utf8"),
"<html>ok</html>\n",
);
assert.equal(
fs.readFileSync(path.join(dest, "assets", "app.js"), "utf8"),
"console.log(1);\n",
);
});
test("copies optional node_modules and package.json for TUI", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openspace-pack-tui-"));
const dist = path.join(tmp, "dist");
const dest = path.join(tmp, "packaged");
const nodeModules = path.join(tmp, "node_modules");
const packageJson = path.join(tmp, "package.json");
fs.mkdirSync(path.join(nodeModules, "ink"), { recursive: true });
fs.writeFileSync(path.join(nodeModules, "ink", "index.js"), "export {};\n");
fs.writeFileSync(packageJson, JSON.stringify({ name: "openspace-tui" }));
fs.mkdirSync(dist, { recursive: true });
fs.writeFileSync(path.join(dist, "index.js"), "console.log('tui');\n");
main([
dist,
dest,
"--node-modules",
nodeModules,
"--package-json",
packageJson,
]);
assert.ok(fs.existsSync(path.join(dest, "index.js")));
assert.ok(fs.existsSync(path.join(dest, "node_modules", "ink", "index.js")));
assert.equal(
JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8")).name,
"openspace-tui",
);
});
test("fails when dist directory is missing", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openspace-pack-miss-"));
assert.throws(
() => main([path.join(tmp, "missing-dist"), path.join(tmp, "out")]),
/distDir is missing/,
);
});
test("script exists at the path apps/* package.json expects", () => {
const scriptPath = path.join(repoRoot, "scripts", "copy-dist-to-packaged.mjs");
assert.ok(fs.existsSync(scriptPath), `missing ${scriptPath}`);
});