From f482c3c71c6549c43a5cba3e5b9b78c623ba155a Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:59:57 +0000 Subject: [PATCH 1/4] build: package code plugins via npm dist in CI --- .github/workflows/nightly.yml | 2 - .github/workflows/publish-stable.yml | 2 - Backend/scripts/ensure-built-in-plugins.js | 69 +++++++++++++++++++--- Justfile | 2 +- scripts/tauri-build.js | 32 +++++++++- 5 files changed, 92 insertions(+), 15 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 16e3454..1751f66 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -179,8 +179,6 @@ jobs: - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - - name: Install OpenVCS SDK CLI - run: cargo install --locked openvcs-sdk --bin cargo-openvcs - name: Install Linux deps if: matrix.platform == 'ubuntu-24.04' diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml index 2062e18..11f18e9 100644 --- a/.github/workflows/publish-stable.yml +++ b/.github/workflows/publish-stable.yml @@ -69,8 +69,6 @@ jobs: - name: Setup sccache uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - - name: Install OpenVCS SDK CLI - run: cargo install --locked openvcs-sdk --bin cargo-openvcs - name: Install Linux build deps (Ubuntu) if: matrix.platform == 'ubuntu-24.04' diff --git a/Backend/scripts/ensure-built-in-plugins.js b/Backend/scripts/ensure-built-in-plugins.js index d0140c3..3a64614 100644 --- a/Backend/scripts/ensure-built-in-plugins.js +++ b/Backend/scripts/ensure-built-in-plugins.js @@ -7,8 +7,6 @@ const { spawnSync } = require('child_process'); const scriptDir = __dirname; const backendDir = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(backendDir, '..'); -const workspaceRoot = path.resolve(repoRoot, '..'); -const sdkDir = path.join(workspaceRoot, 'SDK'); const pluginSources = path.join(backendDir, 'built-in-plugins'); const pluginBundles = path.join(repoRoot, 'target', 'openvcs', 'built-in-plugins'); const nodeRuntimeDir = path.join(repoRoot, 'target', 'openvcs', 'node-runtime'); @@ -16,6 +14,12 @@ const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const skipDirs = new Set(['target', '.git', 'node_modules', 'dist']); +/** + * Returns the newest file mtime (ms) under a directory, ignoring known build dirs. + * + * @param {string} dir - Directory to scan. + * @returns {number|null} Latest mtime or null when no files are present. + */ function latestSourceTime(dir) { let latest = 0; let hasFile = false; @@ -50,6 +54,12 @@ function latestSourceTime(dir) { return hasFile ? latest : null; } +/** + * Resolves the canonical built-in plugin bundle filename from plugin metadata. + * + * @param {string} name - Plugin source directory name. + * @returns {string} Expected `.ovcsp` filename. + */ function bundleFileNameForPlugin(name) { const manifestPath = path.join(pluginSources, name, 'openvcs.plugin.json'); try { @@ -169,24 +179,65 @@ function ensurePluginDependencies(pluginDir) { } } +/** + * Copies plugin archive(s) created by `npm run dist` into the app bundle output. + * + * @param {string} pluginName - Plugin directory name. + * @param {string} pluginDir - Plugin directory path. + */ +function copyPackagedBundles(pluginName, pluginDir) { + const distDir = path.join(pluginDir, 'dist'); + if (!fs.existsSync(distDir)) { + console.error(`Missing dist directory for ${pluginName}: ${distDir}`); + process.exit(1); + } + + const archiveEntries = fs + .readdirSync(distDir, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.ovcsp')); + + if (archiveEntries.length === 0) { + console.error(`No .ovcsp bundle produced for ${pluginName} in ${distDir}`); + process.exit(1); + } + + const preferredName = bundleFileNameForPlugin(pluginName); + const preferred = archiveEntries.find((entry) => entry.name === preferredName); + const sourceArchive = preferred + ? path.join(distDir, preferred.name) + : path.join(distDir, archiveEntries[0].name); + const destArchive = path.join(pluginBundles, preferredName); + + fs.copyFileSync(sourceArchive, destArchive); + console.log(`Built-in plugin bundle copied -> ${destArchive}`); +} + function runDistCommand(pluginNames) { console.log(`Built-in plugin bundles need rebuilding: ${pluginNames.join(', ')}`); for (const pluginName of pluginNames) { const pluginDir = path.join(pluginSources, pluginName); + const packageJsonPath = path.join(pluginDir, 'package.json'); + + if (!fs.existsSync(packageJsonPath)) { + console.log(`Skipping ${pluginName}: no package.json (non-code plugin).`); + continue; + } + ensurePluginDependencies(pluginDir); - console.log(`Packaging built-in plugin ${pluginName} via SDK CLI...`); - const res = spawnSync( - npmExecutable, - ['--prefix', sdkDir, 'run', 'openvcs', '--', 'dist', '--plugin-dir', pluginDir, '--out', pluginBundles], - { cwd: backendDir, stdio: 'inherit' } - ); + console.log(`Packaging built-in plugin ${pluginName} via npm run dist...`); + const res = spawnSync(npmExecutable, ['run', 'dist'], { + cwd: pluginDir, + stdio: 'inherit', + }); if (res.error) { - console.error(`Failed to run SDK packager for ${pluginName}:`, res.error); + console.error(`Failed to run npm dist for ${pluginName}:`, res.error); process.exit(res.status || 1); } if (res.status !== 0) { process.exit(res.status); } + + copyPackagedBundles(pluginName, pluginDir); } } diff --git a/Justfile b/Justfile index 9d8f079..ae8741b 100644 --- a/Justfile +++ b/Justfile @@ -7,7 +7,7 @@ build target="all": _build_all: _build_client _build_plugins: - cargo openvcs dist --all --plugin-dir Backend/built-in-plugins --out target/openvcs/built-in-plugins + node Backend/scripts/ensure-built-in-plugins.js _build_client: _build_plugins npm --prefix Frontend run build diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index 04823ae..b740a47 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -3,6 +3,12 @@ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); +/** + * Loads key/value pairs from a dotenv-style file into process.env. + * Existing environment values are preserved. + * + * @param {string} filePath - Path to the dotenv file. + */ function loadLocalEnv(filePath) { if (!fs.existsSync(filePath)) return; const content = fs.readFileSync(filePath, 'utf8'); @@ -23,6 +29,12 @@ function loadLocalEnv(filePath) { } } +/** + * Normalizes the Tauri signing key into the format expected by TAURI_SIGNING_PRIVATE_KEY. + * + * @param {string} raw - Raw key content. + * @returns {string} Normalized key value. + */ function normalizeSigningKey(raw) { let key = raw; // Tauri expects TAURI_SIGNING_PRIVATE_KEY to be base64 of the minisign key box text. @@ -38,6 +50,12 @@ function normalizeSigningKey(raw) { return key; } +/** + * Prompts for sensitive input without echoing typed characters. + * + * @param {string} question - Prompt text. + * @returns {Promise} User-provided value. + */ function promptHidden(question) { return new Promise((resolve) => { const stdin = process.stdin; @@ -66,8 +84,19 @@ function promptHidden(question) { }); } +/** + * Returns repository and Backend directories based on this script location. + * + * @returns {{repoRoot: string, backendDir: string}} Build directory paths. + */ +function resolvePaths() { + const repoRoot = path.resolve(__dirname, '..'); + const backendDir = path.join(repoRoot, 'Backend'); + return { repoRoot, backendDir }; +} + async function main() { - const repoRoot = process.cwd(); + const { repoRoot, backendDir } = resolvePaths(); loadLocalEnv(path.join(repoRoot, '.env.tauri.local')); if (process.env.TAURI_SIGNING_PRIVATE_KEY_FILE && !process.env.TAURI_SIGNING_PRIVATE_KEY) { @@ -90,6 +119,7 @@ async function main() { process.env.NO_STRIP = process.env.NO_STRIP || 'true'; const child = spawn('cargo', ['tauri', 'build'], { + cwd: backendDir, stdio: 'inherit', env: process.env, }); From 46dc0f01035a1c9c9e586f3d4068e74735de2754 Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:11:16 +0000 Subject: [PATCH 2/4] build: harden built-in plugin bundle copy behavior --- Backend/scripts/ensure-built-in-plugins.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Backend/scripts/ensure-built-in-plugins.js b/Backend/scripts/ensure-built-in-plugins.js index 3a64614..90c43c4 100644 --- a/Backend/scripts/ensure-built-in-plugins.js +++ b/Backend/scripts/ensure-built-in-plugins.js @@ -68,7 +68,8 @@ function bundleFileNameForPlugin(name) { if (pluginId) { return `${pluginId}.ovcsp`; } - } catch { + } catch (e) { + console.warn(`Failed to read manifest for ${name}:`, e); // Fall back to the directory name so the missing/invalid manifest still // forces a rebuild attempt and surfaces the real packaging error later. } @@ -186,6 +187,7 @@ function ensurePluginDependencies(pluginDir) { * @param {string} pluginDir - Plugin directory path. */ function copyPackagedBundles(pluginName, pluginDir) { + fs.mkdirSync(pluginBundles, { recursive: true }); const distDir = path.join(pluginDir, 'dist'); if (!fs.existsSync(distDir)) { console.error(`Missing dist directory for ${pluginName}: ${distDir}`); @@ -208,6 +210,12 @@ function copyPackagedBundles(pluginName, pluginDir) { : path.join(distDir, archiveEntries[0].name); const destArchive = path.join(pluginBundles, preferredName); + if (archiveEntries.length > 1) { + console.warn( + `Multiple .ovcsp archives found for ${pluginName}; using ${path.basename(sourceArchive)}.` + ); + } + fs.copyFileSync(sourceArchive, destArchive); console.log(`Built-in plugin bundle copied -> ${destArchive}`); } @@ -219,7 +227,9 @@ function runDistCommand(pluginNames) { const packageJsonPath = path.join(pluginDir, 'package.json'); if (!fs.existsSync(packageJsonPath)) { - console.log(`Skipping ${pluginName}: no package.json (non-code plugin).`); + console.warn( + `Skipping ${pluginName}: no package.json (non-code plugin; expected to provide prebuilt bundle).` + ); continue; } From 73b260f997e78366969deb47fc23019c17721875 Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:17:53 +0000 Subject: [PATCH 3/4] ci: package built-in plugins before tauri builds --- .github/workflows/nightly.yml | 3 +++ .github/workflows/publish-stable.yml | 3 +++ Backend/scripts/ensure-built-in-plugins.js | 6 ++---- scripts/tauri-build.js | 13 ++++++++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1751f66..55678ef 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -170,6 +170,9 @@ jobs: working-directory: Frontend run: npm run build + - name: Build built-in plugins + run: node Backend/scripts/ensure-built-in-plugins.js + # ---------- Rust & platform deps ---------- - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml index 11f18e9..de80be5 100644 --- a/.github/workflows/publish-stable.yml +++ b/.github/workflows/publish-stable.yml @@ -60,6 +60,9 @@ jobs: working-directory: Frontend run: npm run build + - name: Build built-in plugins + run: node Backend/scripts/ensure-built-in-plugins.js + # ---------- Rust toolchain & deps ---------- - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable diff --git a/Backend/scripts/ensure-built-in-plugins.js b/Backend/scripts/ensure-built-in-plugins.js index 90c43c4..7aec0af 100644 --- a/Backend/scripts/ensure-built-in-plugins.js +++ b/Backend/scripts/ensure-built-in-plugins.js @@ -69,7 +69,7 @@ function bundleFileNameForPlugin(name) { return `${pluginId}.ovcsp`; } } catch (e) { - console.warn(`Failed to read manifest for ${name}:`, e); + console.debug(`Manifest unavailable for ${name}; using directory-name fallback.`, e); // Fall back to the directory name so the missing/invalid manifest still // forces a rebuild attempt and surfaces the real packaging error later. } @@ -227,9 +227,7 @@ function runDistCommand(pluginNames) { const packageJsonPath = path.join(pluginDir, 'package.json'); if (!fs.existsSync(packageJsonPath)) { - console.warn( - `Skipping ${pluginName}: no package.json (non-code plugin; expected to provide prebuilt bundle).` - ); + console.log(`Skipping non-code plugin ${pluginName} (no package.json).`); continue; } diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index b740a47..3139a36 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -90,9 +90,16 @@ function promptHidden(question) { * @returns {{repoRoot: string, backendDir: string}} Build directory paths. */ function resolvePaths() { - const repoRoot = path.resolve(__dirname, '..'); - const backendDir = path.join(repoRoot, 'Backend'); - return { repoRoot, backendDir }; + const cwdRepoRoot = process.cwd(); + const cwdBackendDir = path.join(cwdRepoRoot, 'Backend'); + const cwdTauriConfig = path.join(cwdBackendDir, 'tauri.conf.json'); + if (fs.existsSync(cwdTauriConfig)) { + return { repoRoot: cwdRepoRoot, backendDir: cwdBackendDir }; + } + + const scriptRepoRoot = path.resolve(__dirname, '..'); + const scriptBackendDir = path.join(scriptRepoRoot, 'Backend'); + return { repoRoot: scriptRepoRoot, backendDir: scriptBackendDir }; } async function main() { From 1e4f1713bbe0957804d7558e82236e0a86a3491d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Mar 2026 06:10:51 +0000 Subject: [PATCH 4/4] **Plugin Bundles** - Added `--force` rebuild plus docs. Co-authored-by: Jordonbc --- Backend/scripts/ensure-built-in-plugins.js | 32 ++++++++++++++++++---- docs/plugins.md | 16 +++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Backend/scripts/ensure-built-in-plugins.js b/Backend/scripts/ensure-built-in-plugins.js index 7aec0af..e180dc8 100644 --- a/Backend/scripts/ensure-built-in-plugins.js +++ b/Backend/scripts/ensure-built-in-plugins.js @@ -12,6 +12,8 @@ const pluginBundles = path.join(repoRoot, 'target', 'openvcs', 'built-in-plugins const nodeRuntimeDir = path.join(repoRoot, 'target', 'openvcs', 'node-runtime'); const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const forceRebuild = process.argv.includes('--force'); + const skipDirs = new Set(['target', '.git', 'node_modules', 'dist']); /** @@ -98,6 +100,19 @@ function findOutdatedPlugins() { return outdated; } +/** + * Lists all plugin directories regardless of build state. + * + * @returns {string[]} Plugin directory names. + */ +function findAllPlugins() { + if (!fs.existsSync(pluginSources)) return []; + return fs + .readdirSync(pluginSources, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); +} + function ensureBundlesDir() { fs.mkdirSync(pluginBundles, { recursive: true }); } @@ -187,7 +202,7 @@ function ensurePluginDependencies(pluginDir) { * @param {string} pluginDir - Plugin directory path. */ function copyPackagedBundles(pluginName, pluginDir) { - fs.mkdirSync(pluginBundles, { recursive: true }); + ensureBundlesDir(); const distDir = path.join(pluginDir, 'dist'); if (!fs.existsSync(distDir)) { console.error(`Missing dist directory for ${pluginName}: ${distDir}`); @@ -221,13 +236,16 @@ function copyPackagedBundles(pluginName, pluginDir) { } function runDistCommand(pluginNames) { - console.log(`Built-in plugin bundles need rebuilding: ${pluginNames.join(', ')}`); + const header = forceRebuild + ? `Forcing rebuild of built-in plugins: ${pluginNames.join(', ')}` + : `Built-in plugin bundles need rebuilding: ${pluginNames.join(', ')}`; + console.log(header); for (const pluginName of pluginNames) { const pluginDir = path.join(pluginSources, pluginName); const packageJsonPath = path.join(pluginDir, 'package.json'); if (!fs.existsSync(packageJsonPath)) { - console.log(`Skipping non-code plugin ${pluginName} (no package.json).`); + console.warn(`Skipping non-code plugin ${pluginName} (no package.json).`); continue; } @@ -253,9 +271,11 @@ ensureBundlesDir(); ensureNodeRuntimeDir(); ensureBundledNodeRuntime(); -const outdated = findOutdatedPlugins(); -if (outdated.length > 0) { - runDistCommand(outdated); +const targets = forceRebuild ? findAllPlugins() : findOutdatedPlugins(); +if (targets.length > 0) { + runDistCommand(targets); +} else if (forceRebuild) { + console.log('Force rebuild requested, but no built-in plugins were found.'); } else { console.log('Built-in plugin bundles are up to date.'); } diff --git a/docs/plugins.md b/docs/plugins.md index 17e48c8..39936cc 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -109,6 +109,22 @@ npx openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist `openvcs dist` runs the build step automatically unless `--no-build` is passed. +## Bundling built-in plugins + +The app ships a handful of built-in plugins. Their `.ovcsp` bundles are rebuilt by +running the helper script from the repository root: + +```bash +node Backend/scripts/ensure-built-in-plugins.js +``` + +The script compares source timestamps against the previously packaged bundles, +installs npm deps if needed, runs `npm run dist` inside each plugin, and copies +the resulting archives into `target/openvcs/built-in-plugins`. Pass `--force` to +rebuild all built-in plugins regardless of timestamps (useful for CI or manual +repackaging). Non-code plugins missing `package.json` are skipped because they +ship prebuilt archives. + Typical Node plugin author modules now look like: ```ts