Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
"use strict";
|
|
|
|
/**
|
|
* Pure sidecar collect/merge helpers for the plugin host.
|
|
* Mirrors domain/pluginSyncSidecar.ts so main-process CJS can load without TS.
|
|
*/
|
|
|
|
const SIDECAR_KINDS = new Set(["settings", "account_baseline", "crdt_baseline"]);
|
|
|
|
function isPluginSyncSidecarKind(value) {
|
|
return typeof value === "string" && SIDECAR_KINDS.has(value);
|
|
}
|
|
|
|
function isCloudSyncablePluginSetting(field) {
|
|
if (!field || field.secret === true) return false;
|
|
return field.sync === true;
|
|
}
|
|
|
|
function settingsSidecarKey(settingId, scope, scopeId) {
|
|
return `${settingId}\0${scope}\0${scopeId}`;
|
|
}
|
|
|
|
function parseSettingsSidecarKey(key) {
|
|
const parts = String(key).split("\0");
|
|
if (parts.length !== 3) return null;
|
|
const [settingId, scope, scopeId] = parts;
|
|
if (!settingId || !scope || !scopeId) return null;
|
|
return { settingId, scope, scopeId };
|
|
}
|
|
|
|
/**
|
|
* Map a settings sidecar key onto the currently declared scope coordinates.
|
|
* Returns null when the declared scope cannot be remapped safely (e.g. host
|
|
* scope without a fresh host id).
|
|
*/
|
|
function resolveSettingsSidecarTarget(field, parsed) {
|
|
const targetScope = typeof field?.scope === "string" && field.scope.length > 0
|
|
? field.scope
|
|
: parsed.scope;
|
|
let targetScopeId = parsed.scopeId;
|
|
if (targetScope === "application") {
|
|
targetScopeId = "application";
|
|
} else if (targetScope === "device") {
|
|
// parseSettingsSidecarKey guarantees a non-empty scopeId when parsed is set.
|
|
targetScopeId = parsed.scope === "device" ? parsed.scopeId : "device";
|
|
} else if (targetScope !== parsed.scope) {
|
|
return null;
|
|
}
|
|
return {
|
|
targetScope,
|
|
targetScopeId,
|
|
nextKey: settingsSidecarKey(parsed.settingId, targetScope, targetScopeId),
|
|
};
|
|
}
|
|
|
|
function collectPluginSyncSidecars(input) {
|
|
const now = input.now ?? Date.now();
|
|
const entries = [];
|
|
const settingsIndex = new Map();
|
|
|
|
for (const existing of input.existingSidecars ?? []) {
|
|
if (!existing || typeof existing.pluginId !== "string" || !isPluginSyncSidecarKind(existing.kind)) {
|
|
continue;
|
|
}
|
|
if (existing.kind !== "settings") {
|
|
entries.push({
|
|
pluginId: existing.pluginId,
|
|
kind: existing.kind,
|
|
key: String(existing.key),
|
|
value: existing.value,
|
|
updatedAt: Number(existing.updatedAt) || now,
|
|
});
|
|
continue;
|
|
}
|
|
const copy = {
|
|
pluginId: existing.pluginId,
|
|
kind: "settings",
|
|
key: String(existing.key),
|
|
value: existing.value,
|
|
updatedAt: Number(existing.updatedAt) || now,
|
|
};
|
|
settingsIndex.set(`${copy.pluginId}\0${copy.key}`, copy);
|
|
}
|
|
|
|
for (const stored of input.storedSettings ?? []) {
|
|
if (!stored || typeof stored.pluginId !== "string" || typeof stored.settingId !== "string") continue;
|
|
const declared = input.declaredSettingsByPlugin.get(stored.pluginId);
|
|
if (!declared) continue;
|
|
const field = declared.find((entry) => entry.id === stored.settingId);
|
|
if (!field || !isCloudSyncablePluginSetting(field)) continue;
|
|
const key = settingsSidecarKey(stored.settingId, stored.scope, stored.scopeId);
|
|
const entry = {
|
|
pluginId: stored.pluginId,
|
|
kind: "settings",
|
|
key,
|
|
value: stored.value,
|
|
updatedAt: Number(stored.updatedAt) || now,
|
|
};
|
|
const mapKey = `${entry.pluginId}\0${entry.key}`;
|
|
const previous = settingsIndex.get(mapKey);
|
|
// Keep a retained sidecar when it is strictly newer than the stored row
|
|
// (e.g. schema-rejected remote apply left sidecar only).
|
|
if (!previous || entry.updatedAt >= previous.updatedAt) {
|
|
settingsIndex.set(mapKey, entry);
|
|
}
|
|
}
|
|
|
|
entries.push(...settingsIndex.values());
|
|
entries.sort((a, b) => {
|
|
if (a.pluginId !== b.pluginId) return a.pluginId < b.pluginId ? -1 : 1;
|
|
if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
|
|
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
});
|
|
|
|
return { version: 1, entries };
|
|
}
|
|
|
|
function mergePluginSyncSidecars(params) {
|
|
const remoteEntries = Array.isArray(params.remote?.entries) ? params.remote.entries : [];
|
|
const merged = new Map();
|
|
|
|
for (const entry of params.local ?? []) {
|
|
if (!entry || typeof entry.pluginId !== "string" || !isPluginSyncSidecarKind(entry.kind)) continue;
|
|
merged.set(`${entry.pluginId}\0${entry.kind}\0${entry.key}`, {
|
|
pluginId: entry.pluginId,
|
|
kind: entry.kind,
|
|
key: String(entry.key),
|
|
value: entry.value,
|
|
updatedAt: Number(entry.updatedAt) || 0,
|
|
});
|
|
}
|
|
|
|
for (const raw of remoteEntries) {
|
|
if (!raw || typeof raw.pluginId !== "string" || !isPluginSyncSidecarKind(raw.kind)) continue;
|
|
if (raw.kind === "settings") {
|
|
const declared = params.declaredSettingsByPlugin?.get(raw.pluginId);
|
|
if (declared) {
|
|
const settingId = String(raw.key).split("\0")[0] ?? "";
|
|
const field = declared.find((entry) => entry.id === settingId);
|
|
if (field && !isCloudSyncablePluginSetting(field)) continue;
|
|
}
|
|
}
|
|
const next = {
|
|
pluginId: raw.pluginId,
|
|
kind: raw.kind,
|
|
key: String(raw.key),
|
|
value: raw.value,
|
|
updatedAt: Number(raw.updatedAt) || 0,
|
|
};
|
|
const mapKey = `${next.pluginId}\0${next.kind}\0${next.key}`;
|
|
const previous = merged.get(mapKey);
|
|
if (!previous || next.updatedAt >= previous.updatedAt) {
|
|
merged.set(mapKey, next);
|
|
}
|
|
}
|
|
|
|
return [...merged.values()].sort((a, b) => {
|
|
if (a.pluginId !== b.pluginId) return a.pluginId < b.pluginId ? -1 : 1;
|
|
if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
|
|
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
});
|
|
}
|
|
|
|
function excludeSecretPluginSettingsFromSidecars(entries, declaredSettingsByPlugin) {
|
|
return entries.filter((entry) => {
|
|
if (entry.kind !== "settings") return true;
|
|
const declared = declaredSettingsByPlugin.get(entry.pluginId);
|
|
if (!declared) return true;
|
|
const parsed = parseSettingsSidecarKey(entry.key);
|
|
if (!parsed) return false;
|
|
const field = declared.find((item) => item.id === parsed.settingId);
|
|
if (!field) return true;
|
|
return isCloudSyncablePluginSetting(field);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
collectPluginSyncSidecars,
|
|
excludeSecretPluginSettingsFromSidecars,
|
|
isCloudSyncablePluginSetting,
|
|
isPluginSyncSidecarKind,
|
|
mergePluginSyncSidecars,
|
|
parseSettingsSidecarKey,
|
|
resolveSettingsSidecarTarget,
|
|
settingsSidecarKey,
|
|
};
|