| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- "use strict";
- /**
- * Auto-install the bundled dsh plugins (voice suite) into the web profile.
- *
- * Bundled at build time under <app>/plugins/ together with a manifest.json
- * ({ "<pkg>": { id, name } } — the loader patch entry each plugin needs).
- * At boot we compare with what the profile already carries
- * (~/.dsh/profiles/web) and install/upgrade:
- * - copy each plugin package (deps included) into <profile>/node_modules/
- * - append its insert entry to the profile's cordis.patch.yml
- * New installs require a service restart to take effect — the caller decides
- * whether to restart right away or just notify.
- */
- const fs = require("node:fs");
- const os = require("node:os");
- const path = require("node:path");
- /** Bundled plugin sources (build-time); absent in plain `npm start`. */
- function bundledPluginsRoot() {
- return path.join(__dirname, "plugins");
- }
- /** The web profile directory (DSH_HOME-aware). */
- function webProfileDir() {
- return path.join(process.env.DSH_HOME || path.join(os.homedir(), ".dsh"), "profiles", "web");
- }
- function readJson(file) {
- try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; }
- }
- /** Copy a directory tree (files + symlinks materialized as files). */
- function copyTree(src, dest) {
- fs.mkdirSync(dest, { recursive: true });
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
- const from = path.join(src, entry.name);
- const to = path.join(dest, entry.name);
- if (entry.isDirectory()) copyTree(from, to);
- else fs.copyFileSync(from, to);
- }
- }
- function patchFile(profileDir) {
- return path.join(profileDir, "cordis.patch.yml");
- }
- /** True when the patch yaml already carries an insert entry for this id. */
- function patchHasEntry(patchText, id) {
- return patchText !== null && patchText.includes(`id: ${id}`);
- }
- /**
- * Install/upgrade bundled plugins into the profile (idempotent).
- * @returns {{changed: string[], skipped: string[]}} — "changed" entries need
- * a service restart to take effect.
- */
- function syncPlugins({ log = () => {} } = {}) {
- const bundledRoot = bundledPluginsRoot();
- const result = { changed: [], skipped: [] };
- const manifest = readJson(path.join(bundledRoot, "manifest.json"));
- if (!manifest) return result; // nothing bundled (dev mode) — fine
- const profileDir = webProfileDir();
- const nodeModules = path.join(profileDir, "node_modules");
- try { fs.mkdirSync(nodeModules, { recursive: true }); } catch (err) {
- log(`[plugins] cannot create profile node_modules: ${err.message}`);
- return result;
- }
- const patchText = safeReadPatch(profileDir);
- let patchAppend = "";
- for (const [pkgName, entry] of Object.entries(manifest)) {
- const src = path.join(bundledRoot, pkgName);
- const pkg = readJson(path.join(src, "package.json"));
- if (!pkg || pkg.name !== pkgName || !pkg.version) continue;
- const dest = path.join(nodeModules, pkgName);
- // A symlinked install is a developer-managed layout (DSWorkSpace) —
- // never overwrite it; its source is the live code anyway.
- try {
- if (fs.lstatSync(dest).isSymbolicLink()) { result.skipped.push(pkgName); continue; }
- } catch { /* not installed yet — proceed */ }
- const installed = readJson(path.join(dest, "package.json"));
- const needsInstall = !installed || installed.version !== pkg.version;
- const needsPatch = !entry.dep && !patchHasEntry(patchText, entry.id);
- if (!needsInstall && !needsPatch) { result.skipped.push(pkgName); continue; }
- log(`[plugins] installing ${pkgName}@${pkg.version}${installed ? ` (was ${installed.version})` : ""}`);
- try {
- if (installed) fs.rmSync(dest, { recursive: true, force: true });
- copyTree(src, dest);
- } catch (err) {
- log(`[plugins] copy failed for ${pkgName}: ${err.message}`);
- continue;
- }
- if (needsPatch) {
- patchAppend += ` - id: ${entry.id}\n name: ${entry.name}\n config: {}\n`;
- }
- result.changed.push(pkgName);
- }
- if (patchAppend) {
- try {
- const file = patchFile(profileDir);
- let text = safeReadPatch(profileDir);
- if (text === null || text.trim() === "") {
- text = `# Your patch layer for this dsh profile, applied after every bundle layer:\n# a top-level YAML array of loader patch entries.\n- insert:\n`;
- } else if (!/- insert:/.test(text)) {
- text = `${text.replace(/\s*$/, "\n")}- insert:\n`;
- }
- fs.writeFileSync(file, text.replace(/\n*$/, "\n") + patchAppend);
- } catch (err) {
- log(`[plugins] patch update failed: ${err.message}`);
- }
- }
- return result;
- }
- function safeReadPatch(profileDir) {
- try { return fs.readFileSync(patchFile(profileDir), "utf8"); } catch { return null; }
- }
- module.exports = { syncPlugins, webProfileDir };
|