From 4df52989fa99433b5726e485696cc498256637a0 Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Wed, 29 Jul 2026 02:56:45 +0530 Subject: [PATCH] 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. --- .github/workflows/ci.yml | 22 ++- .gitignore | 7 +- scripts/copy-dist-to-packaged.mjs | 131 ++++++++++++++++++ .../packaging/test_copy_dist_to_packaged.mjs | 80 +++++++++++ 4 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 scripts/copy-dist-to-packaged.mjs create mode 100644 tests/packaging/test_copy_dist_to_packaged.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e66f302..76661cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,27 @@ jobs: - name: Verify PyYAML is importable run: python -c "import yaml; print(yaml.__version__)" - - name: Run test suite + - name: Run Python test suite run: > pytest tests/ -q --tb=short --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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 537eb65..dde8f34 100644 --- a/.gitignore +++ b/.gitignore @@ -68,7 +68,12 @@ tests/entrypoints/* !tests/entrypoints/dashboard/ tests/entrypoints/dashboard/* !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 OPENSPACE.md diff --git a/scripts/copy-dist-to-packaged.mjs b/scripts/copy-dist-to-packaged.mjs new file mode 100644 index 0000000..1ce378d --- /dev/null +++ b/scripts/copy-dist-to-packaged.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node +/** + * Copy a frontend build `dist/` into openspace/packaged/. + * + * Usage: + * node scripts/copy-dist-to-packaged.mjs \ + * [--node-modules ] [--package-json ] + * + * 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 " + + "[--node-modules ] [--package-json ]", + ); + 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 and "); + } + + 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 }; diff --git a/tests/packaging/test_copy_dist_to_packaged.mjs b/tests/packaging/test_copy_dist_to_packaged.mjs new file mode 100644 index 0000000..f70f9ef --- /dev/null +++ b/tests/packaging/test_copy_dist_to_packaged.mjs @@ -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"), "ok\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"), + "ok\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}`); +});