Files
MajoorWaldi-ComfyUI-Majoor-…/ui/entry.ts
T
Ewald 0db328352a fix: close auth bypass on /api route mirror, revert PrimeVue to MIT 4.5.5
Security
--------
ComfyUI's PromptServer.add_routes() mirrors every registered route under a
second "/api" prefix, so /mjr/am/<mutation> is also served at
/api/mjr/am/<mutation>. The auth middleware matched only the canonical
prefix, so the mirrored path reached the same handler with the
authentication check skipped entirely, and without the hardening headers:

    POST /mjr/am/assets/delete      -> requires_auth = True
    POST /api/mjr/am/assets/delete  -> requires_auth = False

Replaced the prefix test with a shared _is_majoor_path() boundary covering
the canonical prefixes, ComfyUI's /api mirror, and the /api/v2/assets
compat surface. Verified it does not capture ComfyUI's own routes. CSRF,
rate limiting and safe-mode gates are per-handler and were unaffected.

Licensing
---------
PrimeVue 5 is no longer MIT: PrimeTek moved it to the commercial "PrimeUI"
license, which requires a key, shows a license notice without one, and
forbids redistributing it "as a component library or development tool"
without a separate OEM License. Dependabot had auto-merged 4.5.5 -> 5.0.1.
This project is GPL-3.0 and commits dist/ to the repo, so shipping it would
nag every user and conflict with the GPL's redistribution requirement.

Pinned back to 4.5.5 (last MIT release). The 5.x bump had touched only
package.json/package-lock.json, so no source depended on v5 APIs. The
revert drops @primeui/license-manager and its @noble/ed25519 key-signature
stack; the primevue chunk shrinks 555.70 -> 520.61 kB. Dependabot is now
configured to ignore primevue major bumps.

Correctness
-----------
- api_v2_assets: stop returning the DB's absolute filepath on this
  network-reachable surface; subfolder + root_id already convey location
  without disclosing local disk layout.
- watcher: enforce _RECENT_GENERATED_HARD_CAP after inserting a batch;
  pruning only beforehand let a large batch overshoot the cap.
- routes/registry: track registered route groups individually so a group
  failing part-way through does not re-register earlier groups on retry.
- fs_walker/scanner: apply the viewer's root check to symlinked files, so
  the scanner stops indexing symlinks whose target escapes every allowed
  root ("ghost assets" the viewer then refuses to open). Consulted only for
  actual symlinks, so ordinary scans pay no extra I/O.
- routes/registry_app: skip the redundant app.add_routes() on the live
  PromptServer app; ComfyUI adds the shared table itself after node
  loading, so every core and Majoor route was registered twice at startup.
- Result: unwrap()/unwrap_or()/map() gated on `data is not None`, treating
  a legitimate Ok(None) as failure; gate on `ok` instead.
- custom_roots: replace a leftover debug print() that wrote user file paths
  to stderr on every move request with logger.debug.
- schema: drop the redundant UNIQUE(filepath, source, root_id) index
  (migration m022). assets.filepath is already UNIQUE, so it could never
  reject a row the column constraint does not, nor narrow a lookup - it
  only cost a second B-tree write per asset on the scan hot path.

Frontend
--------
- entry.ts: app.registerExtension() throws on a duplicate name and ComfyUI
  exposes no unregisterExtension, so a live re-import threw before setup()
  ran and the reload-safe teardown never executed. Catch it and re-run
  setup() directly.
- Rebuilt dist/ so both the reload fix and the PrimeVue revert ship.

CI / tooling
------------
- frontend-compat-nightly: the types step died with a bare "cp: cannot
  stat" (upstream's build has not emitted dist/index.d.ts since Aug 26).
  Check both candidate paths and dump dist/ with an actionable message.
- package.json: lenient checks that swallowed their exit code are now
  suffixed :advisory, the plain names are strict, and quality:strict runs
  the full set.

Verified: 1841 Python tests pass (5 skipped, symlink privileges), ruff
clean; tsc --noEmit clean, 831 JS tests pass, eslint clean, build succeeds.
2026-09-04 18:41:48 +02:00

631 lines
23 KiB
TypeScript

/**
* Entry Point - Majoor Assets Manager
*
* Registers the ComfyUI extension and wires up the Vue-based UI.
*
* Vue migration summary
* ---------------------
* - Sidebar and bottom-panel tabs mount isolated Vue 3 + Pinia applications
* (see ui/vue/App.vue and ui/vue/GeneratedFeedApp.vue).
* - Keep-alive is handled by mountKeepAlive() in createVueApp.js - no more
* _mjrKeepAliveMounted DOM flags.
* - Commands are declared on the extension object (declarative API) so ComfyUI
* surfaces them in the palette and shortcut editor automatically.
* - Toast notifications route through app.extensionManager.toast (ComfyUI's
* PrimeVue toast) with a DOM fallback - no changes needed in toast.js as it
* already prefers the native API.
*/
// Side-effect import: starts the very first /list request immediately when this
// module is parsed (well before ComfyUI invokes our `setup()` step 8b). Keep it
// at the very top - its tiny dep set (api/client + endpoints + config) ensures
// nothing heavy runs ahead of it. Result is consumed by useGridLoader.fetchPage.
import "./features/runtime/earlyFetch.js";
// Side-effect: registers the "Send to Asset Manager" right-click menu on
// SaveImage / VHS_VideoCombine etc. Isolated from the Vue runtime so a
// failure here cannot break the asset grid.
import "./integration/comfy_send_to_am.js";
import "./integration/majoor_save_placeholders.js";
import { testAPI, triggerStartupScan } from "./app/bootstrap.js";
import { checkMajoorVersion } from "./app/versionCheck.js";
import { ensureStyleLoaded } from "./app/style.js";
import { buildMajoorSettings, registerMajoorSettings } from "./app/settings.js";
import {
getComfyApi,
setComfyApi,
setComfyApp,
waitForComfyApi,
waitForComfyApp,
} from "./app/comfyApiBridge.js";
import { EVENTS } from "./app/events.js";
import { ensureFloatingViewerProgressTracking } from "./features/viewer/floatingViewerProgress.js";
import {
NODE_STREAM_FEATURE_ENABLED,
NODE_STREAM_REACTIVATION_DOC,
} from "./features/viewer/nodeStream/nodeStreamFeatureFlag.js";
import {
loadAssets,
upsertAsset,
upsertAssetNow,
removeAssetsFromGrid,
} from "./features/grid/gridApi.js";
import { getActiveGridContainer } from "./features/panel/panelRuntimeRefs.js";
import {
pushGeneratedAsset,
refreshGeneratedFeedHosts,
} from "./features/bottomPanel/feed/feedHost.js";
import { createExecutionRuntimeController } from "./features/stacks/executionRuntimeController.js";
import { extractOutputFiles } from "./utils/extractOutputFiles.js";
import { post } from "./api/client.js";
import { ENDPOINTS } from "./api/endpoints.js";
import { comfyToast } from "./app/toast.js";
import { t } from "./app/i18n.js";
import { APP_CONFIG } from "./app/config.js";
import {
getRuntimeEnrichmentState,
setRuntimeEnrichmentState,
} from "./stores/runtimeEnrichmentState.js";
import { reportError, mjrDbg } from "./utils/logging.js";
// @ts-ignore - external ComfyUI module
import { app } from "../../scripts/app.js";
import { registerRealtimeListeners } from "./features/runtime/registerRealtimeListeners.js";
import { exposeDebugApis } from "./features/runtime/entryDebugApi.js";
import { installBenignConsoleNoiseFilter } from "./features/runtime/benignConsoleNoise.js";
import { registerGenInfoOverridePicker } from "./features/geninfo/genInfoOverridePicker.js";
import {
buildAboutPageBadges,
buildBottomPanelTabs,
buildNativeCommands,
buildNativeKeybindings,
buildNativeMenuCommands,
getMajoorNodeMenuItems,
getMajoorCanvasMenuItems,
getMajoorSelectionToolboxCommands,
mountGlobalRuntime,
prewarmAssetsSidebar,
registerAssetsSidebar,
registerNativeCommands,
registerNativeKeybindings,
startEarlyFetch,
teardownTopBarMfvButton,
teardownGeneratedFeed,
teardownAssetsSidebar,
teardownGlobalRuntime,
} from "./features/runtime/entryUiRegistration.js";
import {
ENTRY_RUNTIME_KEY,
cleanupEntryRuntime,
installEntryRuntimeController,
registerCleanableListener,
removeApiHandlers,
removeRuntimeWindowHandlers,
} from "./features/runtime/entryRuntimeLifecycle.js";
// -- lazy module handles -------------------------------------------------------
/** @type {import("./features/viewer/LiveStreamTracker.js") | null} */
let liveStreamModule = null;
/** @type {import("./features/viewer/nodeStream/NodeStreamController.js") | null} */
let nodeStreamModule = null;
/** @type {Promise<import("./features/viewer/floatingViewerManager.js")> | null} */
let floatingViewerManagerModulePromise = null;
function loadFloatingViewerManagerModule() {
if (!floatingViewerManagerModulePromise) {
floatingViewerManagerModulePromise = import("./features/viewer/floatingViewerManager.js");
}
return floatingViewerManagerModulePromise;
}
function teardownLoadedFloatingViewerManager() {
if (!floatingViewerManagerModulePromise) return;
void floatingViewerManagerModulePromise
.then((mod) => mod?.teardownFloatingViewerManager?.())
.catch((e) => console.debug?.("[Majoor] MFV teardown skipped", e));
}
// -- constants -----------------------------------------------------------------
const SIDEBAR_TAB_ID = "majoor-assets";
const EXECUTION_RUNTIME_KEY = "__MJR_EXECUTION_RUNTIME__";
const EXTENSION_NAME = "Majoor.AssetsManager";
let _lastExecutionBackendSync = { active: null, promptId: "" };
let _deferredGridReloadTimer = null;
const NODE_STREAM_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const NODE_STREAM_HASH_RE = /^[0-9a-f]{20,}$/i;
function _nodeStreamFirstText(...values) {
for (const value of values) {
const text = String(value || "").trim();
if (text) return text;
}
return "";
}
function _isOpaqueNodeStreamType(type) {
const text = String(type || "").trim();
return NODE_STREAM_UUID_RE.test(text) || NODE_STREAM_HASH_RE.test(text);
}
function _getNodeStreamTitle(node) {
return _nodeStreamFirstText(
node?.title,
node?.properties?.title,
node?.properties?.name,
node?.properties?.label,
node?.name,
);
}
function _getNodeStreamGraphNodes(graph) {
if (!graph || typeof graph !== "object") return [];
if (Array.isArray(graph.nodes)) return graph.nodes.filter(Boolean);
if (Array.isArray(graph._nodes)) return graph._nodes.filter(Boolean);
const byId = graph._nodes_by_id ?? graph.nodes_by_id ?? null;
if (byId instanceof Map) return Array.from(byId.values()).filter(Boolean);
if (byId && typeof byId === "object") return Object.values(byId).filter(Boolean);
return [];
}
function _nodeStreamHasGraphNodes(graph) {
return _getNodeStreamGraphNodes(graph).length > 0;
}
function _isNodeStreamSubgraph(node) {
if (!node || typeof node !== "object") return false;
const candidates = [
node.subgraph,
node._subgraph,
node.subgraph?.graph,
node.subgraph?.lgraph,
node.properties?.subgraph,
node.subgraph_instance,
node.subgraph_instance?.graph,
node.inner_graph,
node.subgraph_graph,
];
if (candidates.some(_nodeStreamHasGraphNodes)) return true;
if (Array.isArray(node.nodes) && node.nodes.length > 0 && node.nodes !== node.graph?.nodes) {
return true;
}
return _isOpaqueNodeStreamType(node.type) && Boolean(_getNodeStreamTitle(node));
}
function _getNodeStreamClassLabel(node, fallbackClassType) {
const type = String(node?.type || fallbackClassType || "").trim();
const title = _getNodeStreamTitle(node);
if (_isOpaqueNodeStreamType(type)) return title || "Subgraph";
return type || title || "Node";
}
// -- execution runtime helpers -------------------------------------------------
function ensureExecutionRuntime() {
try {
if (typeof window === "undefined") {
return {
active_prompt_id: null,
queue_remaining: null,
progress_node: null,
progress_value: null,
progress_max: null,
cached_nodes: [],
};
}
if (!window[EXECUTION_RUNTIME_KEY] || typeof window[EXECUTION_RUNTIME_KEY] !== "object") {
window[EXECUTION_RUNTIME_KEY] = {
active_prompt_id: null,
queue_remaining: null,
progress_node: null,
progress_value: null,
progress_max: null,
cached_nodes: [],
};
}
return window[EXECUTION_RUNTIME_KEY];
} catch (e) {
console.debug?.(e);
return {};
}
}
function emitRuntimeStatus(partial = {}) {
try {
const runtime = ensureExecutionRuntime();
Object.assign(runtime, partial || {});
window.dispatchEvent(new CustomEvent(EVENTS.RUNTIME_STATUS, { detail: { ...(runtime as any) } }));
} catch (e) {
console.debug?.(e);
}
}
async function syncExecutionBackendState({ active, promptId = "" }: Record<string, any> = {}) {
const nextActive = !!active;
const nextPromptId = String(promptId || "").trim();
if (
_lastExecutionBackendSync.active === nextActive &&
_lastExecutionBackendSync.promptId === nextPromptId
) {
return;
}
_lastExecutionBackendSync = { active: nextActive, promptId: nextPromptId };
try {
await post(ENDPOINTS.RUNTIME_EXECUTION, {
active: nextActive,
prompt_id: nextPromptId || undefined,
cooldown_ms: Number(APP_CONFIG.EXECUTION_IDLE_GRACE_MS) || 6000,
});
} catch (e) {
reportError(e, "entry.execution_state_sync");
}
}
function isExecutionActive() {
try {
return !!String((ensureExecutionRuntime() as any)?.active_prompt_id || "").trim();
} catch (e) {
console.debug?.(e);
return false;
}
}
function scheduleGridReloadWhenIdle(delayMs = 1200) {
try {
if (_deferredGridReloadTimer) {
clearTimeout(_deferredGridReloadTimer);
}
} catch (e) {
console.debug?.(e);
}
_deferredGridReloadTimer = setTimeout(
() => {
_deferredGridReloadTimer = null;
if (isExecutionActive()) {
scheduleGridReloadWhenIdle(delayMs);
return;
}
const grid = getActiveGridContainer();
if (grid) {
loadAssets(grid);
}
},
Math.max(250, Number(delayMs) || 0),
);
}
// -- lazy module initialisation -------------------------------------------------
function setupLazyModules(runtimeApp) {
void import("./features/viewer/LiveStreamTracker.js")
.then((mod) => {
liveStreamModule = mod;
try {
mod.initLiveStreamTracker(runtimeApp);
} catch (e) {
console.warn("[MJR setup] initLiveStreamTracker failed:", e);
}
})
.catch((e) => console.warn("[MJR setup] LiveStreamTracker load failed:", e));
if (!NODE_STREAM_FEATURE_ENABLED) {
console.debug("[Majoor] Node Stream disabled by feature flag.");
return;
}
void import("./features/viewer/nodeStream/NodeStreamController.js")
.then((mod) => {
nodeStreamModule = mod;
try {
mod.initNodeStream({
app: runtimeApp,
onOutput: (fileData) => {
void loadFloatingViewerManagerModule()
.then((mfv) => mfv?.floatingViewerManager?.feedNodeStream?.(fileData))
.catch((e) => console.debug?.("[NodeStream] MFV output failed", e));
},
onStatus: (nodeId, classType) => {
try {
const graph = runtimeApp?.graph ?? runtimeApp?.canvas?.graph ?? null;
const node = graph?.getNodeById?.(Number(nodeId));
const isSubgraph = _isNodeStreamSubgraph(node);
const title = _getNodeStreamTitle(node);
const displayClass = isSubgraph
? "Subgraph"
: _getNodeStreamClassLabel(node, classType);
void loadFloatingViewerManagerModule()
.then((mfv) => {
mfv?.floatingViewerManager?.setNodeStreamSelection?.(
nodeId,
displayClass,
isSubgraph ? title || _getNodeStreamClassLabel(node, classType) : title,
);
})
.catch((err) => console.debug?.("[NodeStream] MFV status failed", err));
} catch (err) {
console.debug?.("[NodeStream] onStatus failed", err);
}
},
});
} catch (e) {
console.warn("[MJR setup] initNodeStream failed:", e);
}
})
.catch((e) => console.warn("[MJR setup] NodeStream load failed:", e));
}
// -- API listener setup --------------------------------------------------------
async function setupApiListeners(runtimeApp, executionRuntime) {
const api =
(await waitForComfyApi({ app: runtimeApp, timeoutMs: 4000 })) || getComfyApi(runtimeApp);
setComfyApi(api || null);
if (!api) {
console.warn("Majoor API not available, real-time updates disabled");
return;
}
void ensureFloatingViewerProgressTracking({ api, app: runtimeApp }).catch((e) =>
console.debug?.("[Majoor] MFV progress tracking init failed", e),
);
let runtime = null;
try {
runtime = typeof window !== "undefined" ? window[ENTRY_RUNTIME_KEY] || null : null;
} catch (e) {
console.debug?.(e);
}
removeApiHandlers(runtime?.api || null, reportError);
if (api !== runtime?.api) removeApiHandlers(api, reportError);
removeRuntimeWindowHandlers(runtime, reportError);
await registerRealtimeListeners({
api,
runtime,
executionRuntime,
appRef: app,
liveStreamModule,
ensureExecutionRuntime,
emitRuntimeStatus,
getActiveGridContainer,
pushGeneratedAsset,
upsertAsset,
upsertAssetNow,
removeAssetsFromGrid,
getEnrichmentState: getRuntimeEnrichmentState,
setEnrichmentState: setRuntimeEnrichmentState,
comfyToast,
t,
reportError,
registerCleanableListener,
syncExecutionBackendState,
});
}
// -- execution runtime controller ----------------------------------------------
const executionRuntime = createExecutionRuntimeController({
post,
ENDPOINTS,
reportError,
extractOutputFiles,
ensureExecutionRuntime,
emitRuntimeStatus,
refreshGeneratedFeedHosts,
getActiveGridContainer,
});
// -- extension registration -----------------------------------------------------
// Build the extension object once so it can be re-used if `registerExtension`
// throws below (see try/catch after the object literal).
const majoorExtension = {
name: EXTENSION_NAME,
/**
* Declarative settings - registered with ComfyUI's settings panel.
* Replaces the imperative registerMajoorSettings() call for the settings UI.
*/
settings: buildMajoorSettings(app),
/**
* Declarative commands - ComfyUI surfaces these in the command palette
* and the shortcut editor without additional imperative calls.
*/
commands: buildNativeCommands(app, {
sidebarTabId: SIDEBAR_TAB_ID,
triggerStartupScan,
}),
keybindings: buildNativeKeybindings(),
menuCommands: buildNativeMenuCommands(),
aboutPageBadges: buildAboutPageBadges(),
/**
* Declarative bottom-panel tabs - processed by ComfyUI's extension service.
* Each tab's render/destroy callbacks mount/unmount the Vue app via
* mountKeepAlive() so state survives panel collapse/expand cycles.
*/
bottomPanelTabs: buildBottomPanelTabs(),
// -- lifecycle --------------------------------------------------------------
async setup() {
// 1. Runtime lifecycle controller (handles extension cleanup on reload).
installEntryRuntimeController({
cleanupEntryRuntimeFn: cleanupEntryRuntime,
teardownLiveStreamTracker: (runtimeApp) =>
liveStreamModule?.teardownLiveStreamTracker(runtimeApp),
teardownNodeStream: (runtimeApp) => nodeStreamModule?.teardownNodeStream(runtimeApp),
teardownFloatingViewerManager: teardownLoadedFloatingViewerManager,
teardownGeneratedFeed,
teardownAssetsSidebar,
teardownGlobalRuntime,
teardownTopBarMfvButton,
reportError,
});
installBenignConsoleNoiseFilter();
// 2. Resolve the ComfyUI app reference (may take a few frames to be ready).
setComfyApp(app);
const runtimeApp = (await waitForComfyApp({ timeoutMs: 12000 })) || app;
setComfyApp(runtimeApp);
registerGenInfoOverridePicker(runtimeApp);
try {
if (typeof window !== "undefined") (window as any).__MJR_RUNTIME_APP__ = runtimeApp;
} catch (e) {
console.debug?.(e);
}
// 3. Baseline checks.
testAPI();
ensureStyleLoaded({ enabled: true });
mountGlobalRuntime();
// 4. Global runtime - viewer + DnD stay mounted from the always-on Vue runtime.
// 5. Lazy viewer modules (non-blocking).
setupLazyModules(runtimeApp);
// 6. Settings runtime init (applies settings to APP_CONFIG, starts watcher sync...).
registerMajoorSettings(runtimeApp, () => {
const grid = getActiveGridContainer();
if (!grid) return;
if (APP_CONFIG.DEFER_GRID_FETCH_DURING_EXECUTION && isExecutionActive()) {
scheduleGridReloadWhenIdle();
return;
}
loadAssets(grid);
});
// 7. Fallback command registration for older ComfyUI versions that don't
// process the declarative `commands` array on the extension object.
registerNativeCommands(runtimeApp, {
sidebarTabId: SIDEBAR_TAB_ID,
triggerStartupScan,
});
registerNativeKeybindings(runtimeApp);
// 8. Version check (deferred so it doesn't slow startup).
setTimeout(() => {
void checkMajoorVersion();
}, 5000);
// 9. Proactive early fetch - start the first assets page in the background
// so the data is ready (or already in-flight) when the user opens the
// sidebar. Fire immediately: previous 800ms timeout was wasted latency
// that delayed the first paint when the user clicked the sidebar fast.
// startEarlyFetch() is a no-op when ComfyUI is executing a prompt and
// its TTL is wide enough to survive slow extension registration.
try {
startEarlyFetch();
} catch (e) {
console.debug?.(e);
}
// 9. WebSocket / realtime listeners.
void setupApiListeners(runtimeApp, executionRuntime).catch((e) =>
reportError(e, "entry.api_setup"),
);
// 10. Register sidebar tab (Vue app mounts on first open via mountKeepAlive).
if (
registerAssetsSidebar(runtimeApp, {
sidebarTabId: SIDEBAR_TAB_ID,
})
) {
mjrDbg("[Majoor] Sidebar tab registered (Vue)");
} else {
console.warn(
"Majoor Assets Manager: extensionManager.registerSidebarTab is unavailable",
);
}
// 10b. Prewarm the sidebar Vue app on a detached host during browser
// idle so the click->cards latency drops from ~2 s (cold mount) to one
// DOM frame (host re-attach). See entryUiRegistration.prewarmAssetsSidebar.
try {
const _ric = typeof window !== "undefined" ? window.requestIdleCallback : null;
const _doPrewarm = () => {
try {
prewarmAssetsSidebar();
} catch (e) {
console.debug?.(e);
}
};
if (typeof _ric === "function") {
_ric(_doPrewarm, { timeout: 2500 });
} else {
setTimeout(_doPrewarm, 600);
}
} catch (e) {
console.debug?.(e);
}
// 11. Bottom panel tab is registered declaratively via `bottomPanelTabs`.
// 12. Startup warmup: API/DB/counters first, then idle index scan.
// 12. Debug API surface (development/support).
exposeDebugApis({
resolveNodeStreamModule: async () => {
if (!nodeStreamModule) {
nodeStreamModule =
await import("./features/viewer/nodeStream/NodeStreamController.js");
}
return nodeStreamModule;
},
});
},
// -- hooks ------------------------------------------------------------------
/**
* ComfyUI v1.42+ - fires when individual node outputs are updated during
* a workflow execution. Feeds the execution-stack runtime so the bottom
* feed panel receives live assets.
*/
onNodeOutputsUpdated(nodeOutputs) {
try {
executionRuntime.handleNodeOutputsUpdated(nodeOutputs);
} catch (e) {
console.debug?.("[Majoor] onNodeOutputsUpdated error", e);
}
},
/** Context-menu items injected into ComfyUI node right-click menus. */
getNodeMenuItems(node) {
return getMajoorNodeMenuItems(node, app, { sidebarTabId: SIDEBAR_TAB_ID });
},
/** Context-menu items injected into ComfyUI canvas right-click menus. */
getCanvasMenuItems(canvas) {
void canvas;
return getMajoorCanvasMenuItems(app, {
sidebarTabId: SIDEBAR_TAB_ID,
triggerStartupScan,
});
},
/** ComfyUI v1.10.9+ selection toolbox buttons for trackable canvas nodes. */
getSelectionToolboxCommands(selectedItem) {
return getMajoorSelectionToolboxCommands(selectedItem);
},
};
try {
app.registerExtension(majoorExtension);
} catch (e) {
// ComfyUI's registerExtension() throws "Extension named '...' already
// registered" on a second call for the same name, and there is no
// unregisterExtension API to undo the first registration. That means a
// live re-import of this module (module hot-reload, or a manual
// "reload extension" action without a full page refresh) throws here
// instead of ComfyUI invoking setup() again on the existing
// registration - so the reload-safe teardown/reinit path below would
// otherwise never run. Run setup() directly so it still does.
console.warn(
"[Majoor] registerExtension() threw (extension already registered) - re-running setup() directly",
e,
);
void majoorExtension.setup().catch((setupError: unknown) =>
reportError(setupError, "entry.setup_after_duplicate_registration"),
);
}