Merge remote-tracking branch 'origin/main' into claude/update-error-message
# Conflicts: # package.json
This commit is contained in:
@@ -277,6 +277,52 @@ function isUninstalled(id) {
|
||||
return moduleState(id).uninstalled === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A packaged install that still carries modules in its app folder — every
|
||||
* install made before 1.10.0 does — moves them into the data directory
|
||||
* once, as installed copies, and drops the app-folder copies. Nothing
|
||||
* changes for the operator: the same modules run with the same settings
|
||||
* (config is keyed by module id), and from now on each updates on its own
|
||||
* from the catalog. A compiled helper sitting in a bundled folder travels
|
||||
* with it. A module the operator had uninstalled is simply dropped — the
|
||||
* catalog still offers it, and its settings stay in modules.json.
|
||||
*/
|
||||
function unbundleModules() {
|
||||
if (!PACKAGED || !fs.existsSync(BUNDLED_MODULES_DIR)) return;
|
||||
const installed = new Map(readManifests(INSTALLED_MODULES_DIR, 'installed').map((m) => [m.id, m]));
|
||||
let moved = 0;
|
||||
let failed = 0;
|
||||
for (const man of readManifests(BUNDLED_MODULES_DIR, 'bundled')) {
|
||||
if (isUninstalled(man.id)) continue;
|
||||
const copy = installed.get(man.id);
|
||||
if (copy && compareVersions(copy.version, man.version) >= 0) continue; // the data directory already has as good or better
|
||||
const dest = path.join(INSTALLED_MODULES_DIR, man.id);
|
||||
const staging = path.join(INSTALLED_MODULES_DIR, `.${man.id}.moving`);
|
||||
try {
|
||||
fs.mkdirSync(INSTALLED_MODULES_DIR, { recursive: true });
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
fs.cpSync(man.dir, staging, { recursive: true }); // keeps modes: a compiled helper stays executable
|
||||
fs.rmSync(dest, { recursive: true, force: true });
|
||||
fs.renameSync(staging, dest);
|
||||
moved += 1;
|
||||
console.log(`[proddash] moved bundled module "${man.id}" v${man.version} into the data directory${copy ? ` (over the installed v${copy.version})` : ''}`);
|
||||
} catch (err) {
|
||||
failed += 1;
|
||||
console.error(`[proddash] could not move bundled module "${man.id}" into the data directory:`, err?.message || err);
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
console.warn(`[proddash] ${failed} bundled module${failed === 1 ? '' : 's'} stay in the app folder for now — tried again at the next start`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.rmSync(BUNDLED_MODULES_DIR, { recursive: true, force: true });
|
||||
console.log(`[proddash] the app folder carries no modules now${moved ? ` — ${moved} moved into the data directory` : ''}; the admin page's catalog installs and updates them`);
|
||||
} catch (err) {
|
||||
console.warn("[proddash] could not remove the app folder's modules:", err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
function moduleState(id) {
|
||||
const s = modulesConfig[id];
|
||||
return s && typeof s === 'object' ? s : {};
|
||||
@@ -1247,9 +1293,18 @@ const DATA_DIR_INSIDE_ROOT = (() => {
|
||||
return rel.split(path.sep).join('/') + '/';
|
||||
})();
|
||||
|
||||
/* A packaged install — the macOS app, the Windows bundle: anything that is
|
||||
not a git checkout, the same test the updater uses to choose the archive
|
||||
over git pull — carries no modules of its own. Every module comes from
|
||||
the catalog into the data directory, and the shell's own update never
|
||||
brings one along. A checkout keeps running the modules in its tree, which
|
||||
is what development wants. */
|
||||
const PACKAGED = !fs.existsSync(path.join(ROOT, '.git'));
|
||||
|
||||
function isKeptOnUpdate(rel) {
|
||||
if (rel.startsWith('.git/')) return true;
|
||||
if (rel === 'config/modules.json' || rel.startsWith('config/layouts/')) return true;
|
||||
if (PACKAGED && (rel === 'modules' || rel.startsWith('modules/'))) return true;
|
||||
return Boolean(DATA_DIR_INSIDE_ROOT) && `${rel}/`.startsWith(DATA_DIR_INSIDE_ROOT);
|
||||
}
|
||||
|
||||
@@ -1592,6 +1647,29 @@ async function handleAdminApi(req, res, urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Several at once — a fresh install picking its set. One result per id;
|
||||
// a failure stops nothing else.
|
||||
if (urlPath === '/api/admin/modules/install' && req.method === 'POST') {
|
||||
if (refuseAdminWrite(req, res)) return;
|
||||
let body = {};
|
||||
try {
|
||||
body = JSON.parse((await readBody(req)) || '{}');
|
||||
} catch {
|
||||
return sendJson(res, 400, { error: 'Malformed request.' });
|
||||
}
|
||||
const ids = Array.isArray(body.ids) ? [...new Set(body.ids.map((id) => String(id)))] : [];
|
||||
if (!ids.length) return sendJson(res, 400, { error: 'Nothing to install.' });
|
||||
const results = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
results.push({ ok: true, ...(await installModule(id)) });
|
||||
} catch (err) {
|
||||
results.push({ ok: false, id, error: err?.message || String(err) });
|
||||
}
|
||||
}
|
||||
return sendJson(res, 200, { ok: results.every((r) => r.ok), results });
|
||||
}
|
||||
|
||||
const install = urlPath.match(/^\/api\/admin\/modules\/([^/]+)\/install$/);
|
||||
if (install && req.method === 'POST') {
|
||||
if (refuseAdminWrite(req, res)) return;
|
||||
@@ -2086,6 +2164,7 @@ server.requestTimeout = 0;
|
||||
|
||||
/* ── boot ───────────────────────────────────────────────────────────── */
|
||||
|
||||
unbundleModules();
|
||||
discoverModules();
|
||||
mountAllModules();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user