Bootstrap bundled plugins after legacy updates
This commit is contained in:
parent
9bfca09be5
commit
5ec18ac0c4
@ -1,5 +1,10 @@
|
|||||||
# Lumi changelog
|
# Lumi changelog
|
||||||
|
|
||||||
|
## 0.2.27
|
||||||
|
|
||||||
|
- Completed bundled-plugin synchronization automatically on the first startup after a legacy core-only update, allowing production hosts on 0.2.25 to receive the Companion transcription and Song Overlay plugins in the same update flow.
|
||||||
|
- Added a second snapshot-backed plugin synchronization boundary with automatic restoration, exact-release verification, preserved plugin data, and an idempotent completion marker.
|
||||||
|
|
||||||
## 0.2.26
|
## 0.2.26
|
||||||
|
|
||||||
- Added stable Lumi Companion distribution with a durable per-user installer, shared paired-device authentication, managed OBS integration, preserved identity, user-approved updates, complete legal notices, and matching application/tray branding.
|
- Added stable Lumi Companion distribution with a durable per-user installer, shared paired-device authentication, managed OBS integration, preserved identity, user-approved updates, complete legal notices, and matching application/tray branding.
|
||||||
|
|||||||
@ -30,9 +30,12 @@ new branch.
|
|||||||
## Test an experimental branch
|
## Test an experimental branch
|
||||||
|
|
||||||
First update production on `main` to the latest stable release through
|
First update production on `main` to the latest stable release through
|
||||||
**Update from repository**. Stable `0.2.26` and newer synchronize core and
|
**Update from repository**. Stable `0.2.27` and newer synchronize core and
|
||||||
bundled plugins in one snapshot-backed transaction, so future switches in either
|
bundled plugins in one snapshot-backed transaction. When the update begins on
|
||||||
direction use the same verified workflow.
|
`0.2.25`, the newly started core automatically completes the exact-release
|
||||||
|
bundled-plugin phase under a second rollback snapshot before plugins load, with
|
||||||
|
no repair action required. Future switches in either direction then use the same
|
||||||
|
verified workflow.
|
||||||
If a `0.2.24` host enters Experimental before taking the stable update, the
|
If a `0.2.24` host enters Experimental before taking the stable update, the
|
||||||
guarded compatibility bootstrap completes bundled-plugin synchronization after
|
guarded compatibility bootstrap completes bundled-plugin synchronization after
|
||||||
the older updater installs the newest `experimental-*` branch; expect a second
|
the older updater installs the newest `experimental-*` branch; expect a second
|
||||||
|
|||||||
@ -155,6 +155,12 @@ cannot leave those layers on different releases. Core code still accesses
|
|||||||
optional plugin capabilities through registered framework APIs, and plugins can
|
optional plugin capabilities through registered framework APIs, and plugins can
|
||||||
be repaired, upgraded, or downgraded independently from their version picker.
|
be repaired, upgraded, or downgraded independently from their version picker.
|
||||||
|
|
||||||
|
Hosts updating from a legacy core-only updater complete the bundled-plugin phase
|
||||||
|
at the start of the new core, before plugins are loaded. Lumi verifies the same
|
||||||
|
immutable release ref, takes a second core-and-plugin snapshot, preserves plugin
|
||||||
|
data and local-only plugins, restores automatically if synchronization fails,
|
||||||
|
and records an idempotent per-version completion marker.
|
||||||
|
|
||||||
## Runtime Dependencies
|
## Runtime Dependencies
|
||||||
|
|
||||||
Start Lumi through `run.js` (the `npm start` command does this). Before starting
|
Start Lumi through `run.js` (the `npm start` command does this). Before starting
|
||||||
|
|||||||
@ -14,7 +14,7 @@ editable: false
|
|||||||
Lumi is the core web UI and bot runtime.
|
Lumi is the core web UI and bot runtime.
|
||||||
## Runtime
|
## Runtime
|
||||||
Package: lumi-bot
|
Package: lumi-bot
|
||||||
Version: 0.2.26
|
Version: 0.2.27
|
||||||
## Routes
|
## Routes
|
||||||
- POST /api/diagnostics/v1/run
|
- POST /api/diagnostics/v1/run
|
||||||
- GET /api/events
|
- GET /api/events
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.26",
|
"version": "0.2.27",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.26",
|
"version": "0.2.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.6.0",
|
"adm-zip": "^0.6.0",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.26",
|
"version": "0.2.27",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -2,6 +2,38 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "0.2.27",
|
||||||
|
"ref": "refs/tags/v0.2.27",
|
||||||
|
"released_at": "2026-07-24",
|
||||||
|
"installable": true,
|
||||||
|
"rollback_safe": true,
|
||||||
|
"replaces_versions": [
|
||||||
|
"1.2.0"
|
||||||
|
],
|
||||||
|
"data_policy": "preserve",
|
||||||
|
"dependency_policy": "sync_on_restart",
|
||||||
|
"migration_notes": "Completes exact-release bundled-plugin synchronization automatically after an update performed by the legacy 0.2.25 core-only updater. The second phase is snapshot-backed, restores automatically on failure, preserves plugin data and local-only plugins, and is idempotent after completion.",
|
||||||
|
"plugins": {
|
||||||
|
"auto-vc": "0.1.6",
|
||||||
|
"birthday": "0.1.3",
|
||||||
|
"economy-framework": "0.2.10",
|
||||||
|
"economy-games": "0.1.7",
|
||||||
|
"expression-interaction": "0.2.1",
|
||||||
|
"lumi_ai": "0.8.5",
|
||||||
|
"lumi_transcription": "0.1.0",
|
||||||
|
"moderation": "0.1.5",
|
||||||
|
"now_playing": "0.1.2",
|
||||||
|
"okf": "0.1.2",
|
||||||
|
"quotes": "0.1.2",
|
||||||
|
"sample-plugin": "0.1.0",
|
||||||
|
"throne_wishlist": "0.1.2",
|
||||||
|
"welcome_messages": "0.1.1"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"lumi_ai_web_search": "0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.2.26",
|
"version": "0.2.26",
|
||||||
"ref": "refs/tags/v0.2.26",
|
"ref": "refs/tags/v0.2.26",
|
||||||
|
|||||||
@ -4,9 +4,9 @@ const path = require("path");
|
|||||||
const { findSafeTarget } = require("../src/services/versioning");
|
const { findSafeTarget } = require("../src/services/versioning");
|
||||||
|
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const releaseVersion = "0.2.26";
|
const releaseVersion = "0.2.27";
|
||||||
const previousStableVersion = "0.2.25";
|
const previousStableVersion = "0.2.26";
|
||||||
const priorStableVersion = "0.2.24";
|
const priorStableVersion = "0.2.25";
|
||||||
const earliestCompatibleCoreVersion = "0.1.9";
|
const earliestCompatibleCoreVersion = "0.1.9";
|
||||||
const introducedPlugins = {
|
const introducedPlugins = {
|
||||||
lumi_transcription: { version: "0.1.0", knowledge: "lumi-transcription" },
|
lumi_transcription: { version: "0.1.0", knowledge: "lumi-transcription" },
|
||||||
@ -82,4 +82,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
|
|||||||
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
||||||
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
||||||
|
|
||||||
console.log("Release metadata verification passed: stable core 0.2.26 after 0.2.25 with synchronized Companion plugin metadata.");
|
console.log("Release metadata verification passed: stable core 0.2.27 after 0.2.26 with synchronized Companion plugin metadata.");
|
||||||
|
|||||||
@ -13,7 +13,10 @@ const {
|
|||||||
parseDeploymentBranches
|
parseDeploymentBranches
|
||||||
} = require("../src/services/update-repository");
|
} = require("../src/services/update-repository");
|
||||||
const { buildCorePreservePaths } = require("../src/services/update-manager");
|
const { buildCorePreservePaths } = require("../src/services/update-manager");
|
||||||
const { pendingDeploymentBranch } = require("../src/services/updater");
|
const {
|
||||||
|
pendingBundledPluginSync,
|
||||||
|
pendingDeploymentBranch
|
||||||
|
} = require("../src/services/updater");
|
||||||
|
|
||||||
function readJson(relativePath) {
|
function readJson(relativePath) {
|
||||||
return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8"));
|
return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8"));
|
||||||
@ -21,7 +24,7 @@ function readJson(relativePath) {
|
|||||||
|
|
||||||
const releaseIndex = readJson("release-index.json");
|
const releaseIndex = readJson("release-index.json");
|
||||||
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
||||||
assert.deepEqual(releaseVersions, ["0.2.26", "0.2.25", "0.2.24", "0.2.23", "0.2.22", "0.2.21", "0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
assert.deepEqual(releaseVersions, ["0.2.27", "0.2.26", "0.2.25", "0.2.24", "0.2.23", "0.2.22", "0.2.21", "0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
||||||
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
||||||
for (const release of releaseIndex.releases) {
|
for (const release of releaseIndex.releases) {
|
||||||
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
||||||
@ -34,8 +37,8 @@ const packageVersion = readJson("package.json").version;
|
|||||||
const coreManifest = readJson("update-manifest.json");
|
const coreManifest = readJson("update-manifest.json");
|
||||||
assert.equal(packageVersion, coreManifest.version);
|
assert.equal(packageVersion, coreManifest.version);
|
||||||
assert.equal(coreManifest.channel, "stable");
|
assert.equal(coreManifest.channel, "stable");
|
||||||
assert.equal(packageVersion, "0.2.26");
|
assert.equal(packageVersion, "0.2.27");
|
||||||
assert.equal(currentRelease.version, "0.2.26");
|
assert.equal(currentRelease.version, "0.2.27");
|
||||||
assert.deepEqual(currentRelease.replaces_versions, ["1.2.0"]);
|
assert.deepEqual(currentRelease.replaces_versions, ["1.2.0"]);
|
||||||
for (const [pluginId, version] of Object.entries(currentRelease.plugins)) {
|
for (const [pluginId, version] of Object.entries(currentRelease.plugins)) {
|
||||||
assert.equal(readJson(`plugins/${pluginId}/plugin.json`).version, version, `${pluginId} release catalog version`);
|
assert.equal(readJson(`plugins/${pluginId}/plugin.json`).version, version, `${pluginId} release catalog version`);
|
||||||
@ -47,6 +50,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
|
|||||||
const baseTarget = {
|
const baseTarget = {
|
||||||
current_version: "0.2.4",
|
current_version: "0.2.4",
|
||||||
available_versions: [
|
available_versions: [
|
||||||
|
{ version: "0.2.27", ref: "refs/tags/v0.2.27", rollback_safe: true },
|
||||||
{ version: "0.2.26", ref: "refs/tags/v0.2.26", rollback_safe: true },
|
{ version: "0.2.26", ref: "refs/tags/v0.2.26", rollback_safe: true },
|
||||||
{ version: "0.2.25", ref: "refs/tags/v0.2.25", rollback_safe: true },
|
{ version: "0.2.25", ref: "refs/tags/v0.2.25", rollback_safe: true },
|
||||||
{ version: "0.2.24", ref: "refs/tags/v0.2.24", rollback_safe: true },
|
{ version: "0.2.24", ref: "refs/tags/v0.2.24", rollback_safe: true },
|
||||||
@ -109,6 +113,31 @@ assert.equal(pendingDeploymentBranch({ branch: "experimental-ux" }, "experimenta
|
|||||||
assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "experimental-ux" }, "experimental", "0.2.25"), null, "completed deployments must not loop");
|
assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "experimental-ux" }, "experimental", "0.2.25"), null, "completed deployments must not loop");
|
||||||
assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "main" }, "experimental", "0.2.25"), "experimental-ux", "stable re-entry must complete the experimental deployment");
|
assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "main" }, "experimental", "0.2.25"), "experimental-ux", "stable re-entry must complete the experimental deployment");
|
||||||
assert.equal(pendingDeploymentBranch(completedExperimentalUpdate, "main", "0.2.25"), null, "stable configuration must not bootstrap experimental code");
|
assert.equal(pendingDeploymentBranch(completedExperimentalUpdate, "main", "0.2.25"), null, "stable configuration must not bootstrap experimental code");
|
||||||
|
const completedLegacyCoreUpdate = {
|
||||||
|
branch: "refs/tags/v0.2.27",
|
||||||
|
last_update_status: "complete",
|
||||||
|
last_target_kind: "core",
|
||||||
|
last_target_version: "0.2.27",
|
||||||
|
last_snapshot_id: "snapshot-legacy-core"
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
pendingBundledPluginSync(completedLegacyCoreUpdate, "0.2.27"),
|
||||||
|
"refs/tags/v0.2.27",
|
||||||
|
"a core update completed by the legacy updater must synchronize its bundled plugins on startup"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
pendingBundledPluginSync({
|
||||||
|
...completedLegacyCoreUpdate,
|
||||||
|
bundled_plugins_synced_for_version: "0.2.27"
|
||||||
|
}, "0.2.27"),
|
||||||
|
null,
|
||||||
|
"completed bundled-plugin synchronization must be idempotent"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
pendingBundledPluginSync({ branch: "refs/tags/v0.2.27" }, "0.2.27"),
|
||||||
|
null,
|
||||||
|
"repository metadata alone must never trigger startup writes"
|
||||||
|
);
|
||||||
|
|
||||||
const corrected = buildStatus({
|
const corrected = buildStatus({
|
||||||
kind: "core",
|
kind: "core",
|
||||||
@ -119,7 +148,7 @@ const corrected = buildStatus({
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
});
|
});
|
||||||
assert.equal(corrected.version_correction, true);
|
assert.equal(corrected.version_correction, true);
|
||||||
assert.equal(corrected.safe_target_version, "0.2.26");
|
assert.equal(corrected.safe_target_version, "0.2.27");
|
||||||
assert.equal(corrected.update_available, true);
|
assert.equal(corrected.update_available, true);
|
||||||
assert.equal(corrected.blocked, false);
|
assert.equal(corrected.blocked, false);
|
||||||
|
|
||||||
@ -146,6 +175,11 @@ assert.match(
|
|||||||
/async function applyCoreUpdate[\s\S]*include_plugin_code: true[\s\S]*target_plugin_ids: targetPluginIds[\s\S]*applyCoreFilesFromDirectory\(managed\.path\)[\s\S]*applyPluginFiles\(path\.join\(managed\.path, "plugins", pluginId\), pluginId, \{ preserveData: true \}\)[\s\S]*syncPluginRegistry\(\)/,
|
/async function applyCoreUpdate[\s\S]*include_plugin_code: true[\s\S]*target_plugin_ids: targetPluginIds[\s\S]*applyCoreFilesFromDirectory\(managed\.path\)[\s\S]*applyPluginFiles\(path\.join\(managed\.path, "plugins", pluginId\), pluginId, \{ preserveData: true \}\)[\s\S]*syncPluginRegistry\(\)/,
|
||||||
"stable core updates must transactionally carry bundled plugins and newly introduced Companion plugins"
|
"stable core updates must transactionally carry bundled plugins and newly introduced Companion plugins"
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
repoUpdateSource,
|
||||||
|
/async function synchronizeBundledPluginsForCompletedCoreUpdate[\s\S]*ensureManagedRepo\(configuredRemote, sourceRef\)[\s\S]*include_plugin_code: true[\s\S]*applyPluginFiles\(path\.join\(managed\.path, "plugins", pluginId\), pluginId, \{ preserveData: true \}\)[\s\S]*bundled_plugins_synced_for_version: currentVersion[\s\S]*restoreSnapshot\(snapshotRecord\.id/,
|
||||||
|
"legacy core updates must complete bundled-plugin synchronization transactionally on startup"
|
||||||
|
);
|
||||||
const updateManagerSource = fs.readFileSync(path.join(root, "src", "services", "update-manager.js"), "utf8");
|
const updateManagerSource = fs.readFileSync(path.join(root, "src", "services", "update-manager.js"), "utf8");
|
||||||
assert.match(updateManagerSource, /from_source_branch: snapshot\.metadata\?\.from_source_branch/);
|
assert.match(updateManagerSource, /from_source_branch: snapshot\.metadata\?\.from_source_branch/);
|
||||||
assert.match(updateManagerSource, /target_commit: snapshot\.metadata\?\.target_commit/);
|
assert.match(updateManagerSource, /target_commit: snapshot\.metadata\?\.target_commit/);
|
||||||
@ -157,9 +191,11 @@ const updaterSource = fs.readFileSync(path.join(root, "src", "services", "update
|
|||||||
const mainSource = fs.readFileSync(path.join(root, "src", "main.js"), "utf8");
|
const mainSource = fs.readFileSync(path.join(root, "src", "main.js"), "utf8");
|
||||||
assert.match(updaterSource, /function registerRestartHandler/);
|
assert.match(updaterSource, /function registerRestartHandler/);
|
||||||
assert.match(updaterSource, /async function completePendingBranchDeployment/);
|
assert.match(updaterSource, /async function completePendingBranchDeployment/);
|
||||||
|
assert.match(updaterSource, /async function completePendingBundledPluginSync/);
|
||||||
assert.match(updaterSource, /state\.last_update_status === "complete"[\s\S]*state\.last_target_kind === "core"[\s\S]*state\.last_target_version === localVersion/);
|
assert.match(updaterSource, /state\.last_update_status === "complete"[\s\S]*state\.last_target_kind === "core"[\s\S]*state\.last_target_version === localVersion/);
|
||||||
assert.match(mainSource, /registerRestartHandler\(\(\) => shutdown\(10\)\)/);
|
assert.match(mainSource, /registerRestartHandler\(\(\) => shutdown\(10\)\)/);
|
||||||
assert.match(mainSource, /await completePendingBranchDeployment\(configuredRemote, configuredBranch\)/);
|
assert.match(mainSource, /await completePendingBranchDeployment\(configuredRemote, configuredBranch\)/);
|
||||||
|
assert.match(mainSource, /await completePendingBundledPluginSync\(configuredRemote\)/);
|
||||||
assert.match(mainSource, /overlayConnectorManager\.stop\(\)[\s\S]*stopPlugins\(\)[\s\S]*process\.exit\(Number\(exitCode\) === 10 \? 10 : 0\)/);
|
assert.match(mainSource, /overlayConnectorManager\.stop\(\)[\s\S]*stopPlugins\(\)[\s\S]*process\.exit\(Number\(exitCode\) === 10 \? 10 : 0\)/);
|
||||||
const updatesView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-updates.ejs"), "utf8");
|
const updatesView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-updates.ejs"), "utf8");
|
||||||
assert.match(serverSource, /app\.post\("\/admin\/updates\/core\/deploy-branch"/);
|
assert.match(serverSource, /app\.post\("\/admin\/updates\/core\/deploy-branch"/);
|
||||||
|
|||||||
16
src/main.js
16
src/main.js
@ -11,7 +11,14 @@ const stopPlugins =
|
|||||||
typeof pluginService.stopPlugins === "function"
|
typeof pluginService.stopPlugins === "function"
|
||||||
? pluginService.stopPlugins
|
? pluginService.stopPlugins
|
||||||
: async () => {};
|
: async () => {};
|
||||||
const { checkForUpdates, completePendingBranchDeployment, pullUpdates, registerRestartHandler, requestRestart } = require("./services/updater");
|
const {
|
||||||
|
checkForUpdates,
|
||||||
|
completePendingBranchDeployment,
|
||||||
|
completePendingBundledPluginSync,
|
||||||
|
pullUpdates,
|
||||||
|
registerRestartHandler,
|
||||||
|
requestRestart
|
||||||
|
} = require("./services/updater");
|
||||||
const { createCommandRouter } = require("./services/command-router");
|
const { createCommandRouter } = require("./services/command-router");
|
||||||
const { registerTopCommand } = require("./services/top");
|
const { registerTopCommand } = require("./services/top");
|
||||||
const logger = require("./services/logger");
|
const logger = require("./services/logger");
|
||||||
@ -37,6 +44,13 @@ async function main() {
|
|||||||
requestRestart({ delayMs: 0 });
|
requestRestart({ delayMs: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const completedPluginSync = await completePendingBundledPluginSync(configuredRemote);
|
||||||
|
if (completedPluginSync) {
|
||||||
|
console.log(
|
||||||
|
`Completed bundled plugin synchronization for Lumi ${completedPluginSync.version} ` +
|
||||||
|
`(${completedPluginSync.plugin_ids.length} plugins).`
|
||||||
|
);
|
||||||
|
}
|
||||||
registerCorePlaceholders();
|
registerCorePlaceholders();
|
||||||
logger.hookConsole();
|
logger.hookConsole();
|
||||||
const runtimeLog = logger.createLogger("core:runtime", { category: "lifecycle" });
|
const runtimeLog = logger.createLogger("core:runtime", { category: "lifecycle" });
|
||||||
|
|||||||
@ -205,7 +205,11 @@ async function applyCoreUpdate({ source = "stable", remote = null, version = nul
|
|||||||
last_error: null,
|
last_error: null,
|
||||||
last_snapshot_id: record.id,
|
last_snapshot_id: record.id,
|
||||||
last_target_kind: "core",
|
last_target_kind: "core",
|
||||||
last_target_version: target.safe_target_version
|
last_target_version: target.safe_target_version,
|
||||||
|
bundled_plugins_sync_status: "complete",
|
||||||
|
bundled_plugins_synced_for_version: target.safe_target_version,
|
||||||
|
bundled_plugins_synced_commit: managed.commit,
|
||||||
|
bundled_plugins_synced_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
emitProgress(publish, "update:restart_required", { target: "core" });
|
emitProgress(publish, "update:restart_required", { target: "core" });
|
||||||
emitProgress(publish, "update:complete", { target: "core", snapshot_id: record.id });
|
emitProgress(publish, "update:complete", { target: "core", snapshot_id: record.id });
|
||||||
@ -241,6 +245,107 @@ async function applyCoreUpdate({ source = "stable", remote = null, version = nul
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function synchronizeBundledPluginsForCompletedCoreUpdate({
|
||||||
|
remote = null,
|
||||||
|
sourceRef,
|
||||||
|
expectedVersion
|
||||||
|
} = {}) {
|
||||||
|
return withOperation("core:bundled-plugin-sync", async () => {
|
||||||
|
const currentVersion = verifyCoreFiles(repoRoot, expectedVersion);
|
||||||
|
const configuredRemote = remote || getSetting("git_remote", "origin");
|
||||||
|
let managed = null;
|
||||||
|
let marker = null;
|
||||||
|
let snapshot = null;
|
||||||
|
let snapshotRecord = null;
|
||||||
|
let targetPluginIds = [];
|
||||||
|
let stage = "opening the completed core release";
|
||||||
|
try {
|
||||||
|
managed = ensureManagedRepo(configuredRemote, sourceRef);
|
||||||
|
verifyCoreFiles(managed.path, currentVersion);
|
||||||
|
targetPluginIds = bundledPluginIds(managed.path);
|
||||||
|
for (const pluginId of targetPluginIds) verifyPluginFiles(pluginId, managed.path);
|
||||||
|
|
||||||
|
stage = "preparing a bundled-plugin rollback snapshot";
|
||||||
|
marker = createRecoveryMarker({
|
||||||
|
target_kind: "core",
|
||||||
|
target_id: "bundled_plugins",
|
||||||
|
from_version: currentVersion,
|
||||||
|
to_version: currentVersion,
|
||||||
|
source_branch: managed.branch,
|
||||||
|
update_method: "startup_bundled_plugin_sync",
|
||||||
|
rollback_safe: true,
|
||||||
|
major_crossing: false
|
||||||
|
});
|
||||||
|
updateRecoveryMarker({ status: "applying" });
|
||||||
|
snapshot = await createSnapshot({
|
||||||
|
type: "bot",
|
||||||
|
metadata: {
|
||||||
|
target_kind: "core",
|
||||||
|
target_id: "bundled_plugins",
|
||||||
|
from_version: currentVersion,
|
||||||
|
to_version: currentVersion,
|
||||||
|
source_branch: managed.branch,
|
||||||
|
target_commit: managed.commit,
|
||||||
|
update_method: "startup_bundled_plugin_sync",
|
||||||
|
rollback_safe: true,
|
||||||
|
recovery_marker_id: marker.id,
|
||||||
|
include_plugin_code: true,
|
||||||
|
target_plugin_ids: targetPluginIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
snapshotRecord = finalizeSnapshot(snapshot);
|
||||||
|
|
||||||
|
stage = "synchronizing bundled plugins";
|
||||||
|
for (const pluginId of targetPluginIds) {
|
||||||
|
applyPluginFiles(path.join(managed.path, "plugins", pluginId), pluginId, { preserveData: true });
|
||||||
|
}
|
||||||
|
updateRecoveryMarker({ status: "verifying" });
|
||||||
|
for (const pluginId of targetPluginIds) verifyPluginFiles(pluginId, repoRoot);
|
||||||
|
syncPluginRegistry();
|
||||||
|
markRecoveryMarkerComplete({ snapshot_id: snapshotRecord.id });
|
||||||
|
writeUpdateState({
|
||||||
|
bundled_plugins_sync_status: "complete",
|
||||||
|
bundled_plugins_sync_error: null,
|
||||||
|
bundled_plugins_synced_for_version: currentVersion,
|
||||||
|
bundled_plugins_synced_commit: managed.commit,
|
||||||
|
bundled_plugins_synced_at: new Date().toISOString()
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: "complete",
|
||||||
|
version: currentVersion,
|
||||||
|
source_ref: managed.branch,
|
||||||
|
commit: managed.commit,
|
||||||
|
plugin_ids: targetPluginIds,
|
||||||
|
snapshot: snapshotRecord
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const originalMessage = error?.message || String(error || "Unknown synchronization error.");
|
||||||
|
error.message = `Bundled plugin startup synchronization failed while ${stage}: ${originalMessage}`;
|
||||||
|
if (snapshotRecord) {
|
||||||
|
try {
|
||||||
|
restoreSnapshot(snapshotRecord.id, {
|
||||||
|
expectedType: "bot",
|
||||||
|
currentVersion,
|
||||||
|
allowUnsafeMajorRollback: true
|
||||||
|
});
|
||||||
|
syncPluginRegistry();
|
||||||
|
} catch (restoreError) {
|
||||||
|
error.message += ` Automatic restore also failed: ${restoreError.message}`;
|
||||||
|
}
|
||||||
|
} else if (snapshot) {
|
||||||
|
try { discardSnapshot(snapshot); } catch {}
|
||||||
|
}
|
||||||
|
if (marker) markRecoveryMarkerFailed(error);
|
||||||
|
writeUpdateState({
|
||||||
|
bundled_plugins_sync_status: "failed",
|
||||||
|
bundled_plugins_sync_error: error.message,
|
||||||
|
bundled_plugins_sync_failed_at: new Date().toISOString()
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function deployCoreBranch({ branch, remote = null, publish } = {}) {
|
async function deployCoreBranch({ branch, remote = null, publish } = {}) {
|
||||||
return withOperation("core", async () => {
|
return withOperation("core", async () => {
|
||||||
const targetBranch = normalizeDeploymentBranch(branch);
|
const targetBranch = normalizeDeploymentBranch(branch);
|
||||||
@ -346,7 +451,11 @@ async function deployCoreBranch({ branch, remote = null, publish } = {}) {
|
|||||||
last_snapshot_id: snapshotRecord.id,
|
last_snapshot_id: snapshotRecord.id,
|
||||||
last_target_kind: "core",
|
last_target_kind: "core",
|
||||||
last_target_version: target.version,
|
last_target_version: target.version,
|
||||||
last_deployment_method: "branch_deploy"
|
last_deployment_method: "branch_deploy",
|
||||||
|
bundled_plugins_sync_status: "complete",
|
||||||
|
bundled_plugins_synced_for_version: target.version,
|
||||||
|
bundled_plugins_synced_commit: managed.commit,
|
||||||
|
bundled_plugins_synced_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
emitProgress(publish, "update:restart_required", { target: "core", deployment });
|
emitProgress(publish, "update:restart_required", { target: "core", deployment });
|
||||||
emitProgress(publish, "update:complete", { target: "core", snapshot_id: snapshotRecord.id, deployment });
|
emitProgress(publish, "update:complete", { target: "core", snapshot_id: snapshotRecord.id, deployment });
|
||||||
@ -564,6 +673,7 @@ function disablePluginForRecovery(pluginId, publish) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
applyCoreUpdate,
|
applyCoreUpdate,
|
||||||
|
synchronizeBundledPluginsForCompletedCoreUpdate,
|
||||||
deployCoreBranch,
|
deployCoreBranch,
|
||||||
applyPluginUpdateFromRepo,
|
applyPluginUpdateFromRepo,
|
||||||
targetForRequestedVersion,
|
targetForRequestedVersion,
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
const { getUpdateStatus } = require("./update-index");
|
const { getUpdateStatus } = require("./update-index");
|
||||||
const { applyCoreUpdate, deployCoreBranch } = require("./repo-update");
|
const {
|
||||||
|
applyCoreUpdate,
|
||||||
|
deployCoreBranch,
|
||||||
|
synchronizeBundledPluginsForCompletedCoreUpdate
|
||||||
|
} = require("./repo-update");
|
||||||
const {
|
const {
|
||||||
normalizeRepositoryTarget,
|
normalizeRepositoryTarget,
|
||||||
readUpdateState,
|
readUpdateState,
|
||||||
@ -57,6 +61,32 @@ async function completePendingBranchDeployment(remote, configuredBranch) {
|
|||||||
return deployCoreBranch({ branch: targetBranch, remote });
|
return deployCoreBranch({ branch: targetBranch, remote });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pendingBundledPluginSync(state, localVersion) {
|
||||||
|
const confirmedCoreUpdate =
|
||||||
|
state.last_update_status === "complete" &&
|
||||||
|
state.last_target_kind === "core" &&
|
||||||
|
state.last_target_version === localVersion &&
|
||||||
|
Boolean(state.last_snapshot_id);
|
||||||
|
if (
|
||||||
|
!confirmedCoreUpdate ||
|
||||||
|
state.bundled_plugins_synced_for_version === localVersion
|
||||||
|
) return null;
|
||||||
|
const sourceRef = String(state.branch || "").trim();
|
||||||
|
return sourceRef || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completePendingBundledPluginSync(remote) {
|
||||||
|
const state = readUpdateState();
|
||||||
|
const localVersion = require("../../package.json").version;
|
||||||
|
const sourceRef = pendingBundledPluginSync(state, localVersion);
|
||||||
|
if (!sourceRef) return null;
|
||||||
|
return synchronizeBundledPluginsForCompletedCoreUpdate({
|
||||||
|
remote,
|
||||||
|
sourceRef,
|
||||||
|
expectedVersion: localVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function registerRestartHandler(handler) {
|
function registerRestartHandler(handler) {
|
||||||
restartHandler = typeof handler === "function" ? handler : null;
|
restartHandler = typeof handler === "function" ? handler : null;
|
||||||
return () => {
|
return () => {
|
||||||
@ -81,9 +111,11 @@ function requestRestart(options = {}) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
completePendingBranchDeployment,
|
completePendingBranchDeployment,
|
||||||
|
completePendingBundledPluginSync,
|
||||||
pullUpdates,
|
pullUpdates,
|
||||||
normalizeRepositoryTarget,
|
normalizeRepositoryTarget,
|
||||||
pendingDeploymentBranch,
|
pendingDeploymentBranch,
|
||||||
|
pendingBundledPluginSync,
|
||||||
resolveUpdateBranch,
|
resolveUpdateBranch,
|
||||||
registerRestartHandler,
|
registerRestartHandler,
|
||||||
requestRestart
|
requestRestart
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Lumi Core",
|
"name": "Lumi Core",
|
||||||
"version": "0.2.26",
|
"version": "0.2.27",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"released_at": "2026-07-24",
|
"released_at": "2026-07-24",
|
||||||
"compatible_from": "0.1.9",
|
"compatible_from": "0.1.9",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"replaces_versions": [
|
"replaces_versions": [
|
||||||
"1.2.0"
|
"1.2.0"
|
||||||
],
|
],
|
||||||
"migration_notes": "Adds the stable Lumi Companion, server-hosted transcription, Song Overlay, shared paired-device authentication, and synchronized core-plus-bundled-plugin updates. Existing settings, databases, pairing records, tokens, overlays, uploads, models, secrets, plugin data, and local-only plugins are preserved.",
|
"migration_notes": "Completes bundled-plugin synchronization automatically after updates performed by the legacy 0.2.25 core-only updater. The synchronization is snapshot-backed, exact-release verified, idempotent, and preserves settings, databases, pairing records, tokens, overlays, uploads, models, secrets, plugin data, and local-only plugins.",
|
||||||
"rollback_safe": true,
|
"rollback_safe": true,
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"Node.js 18 or newer"
|
"Node.js 18 or newer"
|
||||||
@ -325,6 +325,18 @@
|
|||||||
],
|
],
|
||||||
"rollback_safe": true,
|
"rollback_safe": true,
|
||||||
"migration_notes": "Adds the shared DesignMotionHQ UX foundation, repaired overlay and resource-library workflows, and verified snapshot-backed switching between main and exact experimental branches. Existing content, settings, tokens, overlays, databases, uploads, models, feedback, secrets, plugin data, and local-only plugins are preserved."
|
"migration_notes": "Adds the shared DesignMotionHQ UX foundation, repaired overlay and resource-library workflows, and verified snapshot-backed switching between main and exact experimental branches. Existing content, settings, tokens, overlays, databases, uploads, models, feedback, secrets, plugin data, and local-only plugins are preserved."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.26",
|
||||||
|
"channel": "stable",
|
||||||
|
"released_at": "2026-07-24",
|
||||||
|
"compatible_from": "0.1.9",
|
||||||
|
"migration_kind": "patch",
|
||||||
|
"replaces_versions": [
|
||||||
|
"1.2.0"
|
||||||
|
],
|
||||||
|
"rollback_safe": true,
|
||||||
|
"migration_notes": "Adds the stable Lumi Companion, server-hosted transcription, Song Overlay, shared paired-device authentication, and synchronized core-plus-bundled-plugin updates. Existing settings, databases, pairing records, tokens, overlays, uploads, models, secrets, plugin data, and local-only plugins are preserved."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user