comfyui-mcp 0.52.103 → 0.52.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/node-management.js +239 -2
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/npx-restart-scope.js +4 -1
- package/dist/services/npx-restart-scope.js.map +1 -1
- package/dist/services/panel-launcher.js +922 -225
- package/dist/services/panel-launcher.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
4
4
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { config, getComfyUIBaseUrl, getComfyuiTargetGeneration, isRemoteMode, } from "../config.js";
|
|
6
6
|
import { comfyuiFetch } from "../comfyui/fetch.js";
|
|
@@ -1881,7 +1881,14 @@ function findInstalledNode(idOrUrl, installed) {
|
|
|
1881
1881
|
/** Directory names a pack could plausibly be installed under for `id`. */
|
|
1882
1882
|
function packDirNameCandidates(id) {
|
|
1883
1883
|
const wanted = id.trim().toLowerCase();
|
|
1884
|
-
|
|
1884
|
+
// A path-shaped enable/disable id must never be reduced to its basename:
|
|
1885
|
+
// doing that would let `../pack` vouch for `custom_nodes/pack` and could
|
|
1886
|
+
// hand an untrusted id to a local mutation path. Git URLs are the one
|
|
1887
|
+
// deliberate exception because their checkout directory is derived from
|
|
1888
|
+
// the URL after it has already passed git-URL validation.
|
|
1889
|
+
const names = new Set();
|
|
1890
|
+
if (!/[/\\]/.test(wanted))
|
|
1891
|
+
names.add(wanted);
|
|
1885
1892
|
if (looksLikeGitUrl(id)) {
|
|
1886
1893
|
names.add(gitCheckoutDir(parseGitUrl(id).baseUrl).toLowerCase());
|
|
1887
1894
|
}
|
|
@@ -1939,6 +1946,196 @@ export function findPackOnDisk(id, comfyuiBase) {
|
|
|
1939
1946
|
}
|
|
1940
1947
|
return { state: "not-found", scanned: customNodes };
|
|
1941
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Validate the id before it can participate in a local filesystem mutation or
|
|
1951
|
+
* become a comfy-cli argument. Installed-pack ids are single directory names;
|
|
1952
|
+
* URLs, separators, option-like names, and control characters are not valid
|
|
1953
|
+
* targets for this recovery path.
|
|
1954
|
+
*/
|
|
1955
|
+
function assertSafeLocalPackId(id) {
|
|
1956
|
+
const trimmed = id.trim();
|
|
1957
|
+
if (trimmed.length === 0 ||
|
|
1958
|
+
trimmed !== id ||
|
|
1959
|
+
trimmed === "." ||
|
|
1960
|
+
trimmed === ".." ||
|
|
1961
|
+
trimmed.startsWith("-") ||
|
|
1962
|
+
/[/\\]/.test(trimmed) ||
|
|
1963
|
+
/[\x00-\x1F\x7F]/.test(trimmed)) {
|
|
1964
|
+
throw new ValidationError(`Refusing to enable local custom node "${id}": expected a single installed ` +
|
|
1965
|
+
`pack id without path separators, control characters, or a leading '-'.`);
|
|
1966
|
+
}
|
|
1967
|
+
return trimmed;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Find a real directory named `<pack>.disabled` under the selected
|
|
1971
|
+
* custom_nodes root. This intentionally does not follow symlink entries and
|
|
1972
|
+
* refuses duplicate active/disabled identities; otherwise a rename could
|
|
1973
|
+
* enable a different pack than the caller named or make ComfyUI load an
|
|
1974
|
+
* arbitrary external tree after restart.
|
|
1975
|
+
*/
|
|
1976
|
+
function findDisabledPackOnDisk(id, comfyuiBase) {
|
|
1977
|
+
const customNodes = join(comfyuiBase, "custom_nodes");
|
|
1978
|
+
let entries;
|
|
1979
|
+
try {
|
|
1980
|
+
if (!existsSync(customNodes))
|
|
1981
|
+
return undefined;
|
|
1982
|
+
entries = readdirSync(customNodes, { withFileTypes: true });
|
|
1983
|
+
}
|
|
1984
|
+
catch {
|
|
1985
|
+
return undefined;
|
|
1986
|
+
}
|
|
1987
|
+
const wanted = new Set([id.toLowerCase()]);
|
|
1988
|
+
let disabled;
|
|
1989
|
+
let activeDir;
|
|
1990
|
+
for (const entry of entries) {
|
|
1991
|
+
const name = entry.name;
|
|
1992
|
+
if (name.startsWith("."))
|
|
1993
|
+
continue;
|
|
1994
|
+
const lower = name.toLowerCase();
|
|
1995
|
+
const isDisabled = lower.endsWith(".disabled");
|
|
1996
|
+
const canonical = isDisabled ? name.slice(0, -".disabled".length) : name;
|
|
1997
|
+
const canonicalLower = canonical.toLowerCase();
|
|
1998
|
+
if (!wanted.has(canonicalLower) &&
|
|
1999
|
+
!(isManagerSelfTarget(id) && isManagerSelfTarget(canonical))) {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
if (entry.isSymbolicLink()) {
|
|
2003
|
+
if (isDisabled) {
|
|
2004
|
+
throw new NodeManagementError(`Refusing to enable "${id}": ${join(customNodes, name)} is a symlink; ` +
|
|
2005
|
+
`the local recovery path only renames a real custom-node directory.`);
|
|
2006
|
+
}
|
|
2007
|
+
throw new NodeManagementError(`Refusing to enable "${id}": the active destination ${join(customNodes, name)} ` +
|
|
2008
|
+
`is a symlink. Remove the collision before retrying.`);
|
|
2009
|
+
}
|
|
2010
|
+
const dir = join(customNodes, name);
|
|
2011
|
+
if (isDisabled) {
|
|
2012
|
+
if (!entry.isDirectory()) {
|
|
2013
|
+
throw new NodeManagementError(`Refusing to enable "${id}": ${dir} is not a real directory.`);
|
|
2014
|
+
}
|
|
2015
|
+
if (disabled) {
|
|
2016
|
+
throw new NodeManagementError(`Refusing to enable "${id}": multiple matching .disabled directories ` +
|
|
2017
|
+
`exist under ${customNodes}.`);
|
|
2018
|
+
}
|
|
2019
|
+
disabled = {
|
|
2020
|
+
disabledDir: dir,
|
|
2021
|
+
enabledDir: join(customNodes, canonical),
|
|
2022
|
+
cliId: canonical,
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
else {
|
|
2026
|
+
// Any existing active-name entry is a collision, including a file. A
|
|
2027
|
+
// POSIX rename could otherwise replace it, and a symlink could redirect
|
|
2028
|
+
// what ComfyUI loads after restart.
|
|
2029
|
+
activeDir = dir;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (disabled && activeDir) {
|
|
2033
|
+
throw new NodeManagementError(`Refusing to enable "${id}": both ${activeDir} and ${disabled.disabledDir} ` +
|
|
2034
|
+
`exist. Remove the duplicate before retrying.`);
|
|
2035
|
+
}
|
|
2036
|
+
return disabled;
|
|
2037
|
+
}
|
|
2038
|
+
function verifyLocalEnable(id, comfyuiBase, enabledDir) {
|
|
2039
|
+
const disabled = findDisabledPackOnDisk(id, comfyuiBase);
|
|
2040
|
+
if (disabled) {
|
|
2041
|
+
throw new NodeManagementError(`The local enable of "${id}" did NOT take effect: ${disabled.disabledDir} ` +
|
|
2042
|
+
`is still disabled.`);
|
|
2043
|
+
}
|
|
2044
|
+
const after = findPackOnDisk(id, comfyuiBase);
|
|
2045
|
+
if (after.state !== "found" ||
|
|
2046
|
+
resolve(after.dir) !== resolve(enabledDir) ||
|
|
2047
|
+
basename(after.dir).toLowerCase().endsWith(".disabled")) {
|
|
2048
|
+
throw new NodeManagementError(`The local enable of "${id}" could NOT be verified under ${join(comfyuiBase, "custom_nodes")}.`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function revalidateLocalEnableTarget(id, comfyuiBase, expected) {
|
|
2052
|
+
const current = findDisabledPackOnDisk(id, comfyuiBase);
|
|
2053
|
+
if (!current ||
|
|
2054
|
+
resolve(current.disabledDir) !== resolve(expected.disabledDir) ||
|
|
2055
|
+
resolve(current.enabledDir) !== resolve(expected.enabledDir)) {
|
|
2056
|
+
throw new NodeManagementError(`Refusing to enable "${id}": the validated source or destination changed ` +
|
|
2057
|
+
`before the local rename. NOTHING was changed by this tool.`);
|
|
2058
|
+
}
|
|
2059
|
+
return current;
|
|
2060
|
+
}
|
|
2061
|
+
/** Return undefined on a verified rollback, otherwise a safe refusal reason. */
|
|
2062
|
+
function rollbackLocalEnable(id, comfyuiBase, disabled) {
|
|
2063
|
+
try {
|
|
2064
|
+
// Do not move a destination that appeared or changed while verification
|
|
2065
|
+
// was running. A rollback is only safe when the original source is absent
|
|
2066
|
+
// and the exact destination is still the directory we created.
|
|
2067
|
+
if (findDisabledPackOnDisk(id, comfyuiBase)) {
|
|
2068
|
+
return "the original .disabled source is still present or a collision appeared";
|
|
2069
|
+
}
|
|
2070
|
+
const active = findPackOnDisk(id, comfyuiBase);
|
|
2071
|
+
if (active.state !== "found" ||
|
|
2072
|
+
resolve(active.dir) !== resolve(disabled.enabledDir) ||
|
|
2073
|
+
basename(active.dir).toLowerCase().endsWith(".disabled")) {
|
|
2074
|
+
return "the destination no longer matches the directory this call renamed";
|
|
2075
|
+
}
|
|
2076
|
+
renameSync(disabled.enabledDir, disabled.disabledDir);
|
|
2077
|
+
const restored = findDisabledPackOnDisk(id, comfyuiBase);
|
|
2078
|
+
if (!restored ||
|
|
2079
|
+
resolve(restored.disabledDir) !== resolve(disabled.disabledDir) ||
|
|
2080
|
+
resolve(restored.enabledDir) !== resolve(disabled.enabledDir)) {
|
|
2081
|
+
return "the rollback rename landed but its .disabled post-state could not be verified";
|
|
2082
|
+
}
|
|
2083
|
+
return undefined;
|
|
2084
|
+
}
|
|
2085
|
+
catch (err) {
|
|
2086
|
+
return `rollback failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function enablePackFromDisk(id, comfyuiBase, disabled) {
|
|
2090
|
+
const target = revalidateLocalEnableTarget(id, comfyuiBase, disabled);
|
|
2091
|
+
try {
|
|
2092
|
+
renameSync(target.disabledDir, target.enabledDir);
|
|
2093
|
+
}
|
|
2094
|
+
catch (err) {
|
|
2095
|
+
throw new NodeManagementError(`Could not enable "${id}" by renaming ${target.disabledDir}: ` +
|
|
2096
|
+
`${err instanceof Error ? err.message : String(err)}. NOTHING was changed by this tool.`);
|
|
2097
|
+
}
|
|
2098
|
+
try {
|
|
2099
|
+
verifyLocalEnable(id, comfyuiBase, target.enabledDir);
|
|
2100
|
+
}
|
|
2101
|
+
catch (err) {
|
|
2102
|
+
const rollbackError = rollbackLocalEnable(id, comfyuiBase, target);
|
|
2103
|
+
const verification = err instanceof Error ? err.message : String(err);
|
|
2104
|
+
throw new NodeManagementError(`The local enable of "${id}" could NOT be verified: ${verification}. ` +
|
|
2105
|
+
(rollbackError === undefined
|
|
2106
|
+
? "The rename was rolled back."
|
|
2107
|
+
: `The rename could NOT be rolled back safely: ${rollbackError}.`));
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
mechanism: "filesystem",
|
|
2111
|
+
message: `Enabled "${id}" by renaming its validated local directory to ` +
|
|
2112
|
+
`${target.enabledDir} (verified on disk). A ComfyUI restart is required ` +
|
|
2113
|
+
`for the change to take effect.`,
|
|
2114
|
+
details: { from: target.disabledDir, to: target.enabledDir },
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
function enablePackViaComfyCliFromDisk(id, comfyuiBase, disabled) {
|
|
2118
|
+
const out = runCmCli(["enable", disabled.cliId], comfyuiBase);
|
|
2119
|
+
try {
|
|
2120
|
+
verifyLocalEnable(id, comfyuiBase, disabled.enabledDir);
|
|
2121
|
+
}
|
|
2122
|
+
catch (err) {
|
|
2123
|
+
if (err instanceof NodeManagementError) {
|
|
2124
|
+
return {
|
|
2125
|
+
mechanism: "comfy-cli",
|
|
2126
|
+
message: err.message + " comfy-cli reported success, but the local post-state disagrees.",
|
|
2127
|
+
details: out.trim(),
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
throw err;
|
|
2131
|
+
}
|
|
2132
|
+
return {
|
|
2133
|
+
mechanism: "comfy-cli",
|
|
2134
|
+
message: `Enabled "${id}" via official comfy-cli (verified on disk at ` +
|
|
2135
|
+
`${disabled.enabledDir}). A ComfyUI restart is required for the change to take effect.`,
|
|
2136
|
+
details: out.trim(),
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
1942
2139
|
export function capturePackPresenceContext(diskRoot, options = {}) {
|
|
1943
2140
|
const remote = isRemoteMode();
|
|
1944
2141
|
return {
|
|
@@ -3557,6 +3754,20 @@ async function setCustomNodeEnabled(opts, enable) {
|
|
|
3557
3754
|
// during an await below (codex gate round 5).
|
|
3558
3755
|
const cliWorkspace = resolveEffectiveComfyUIBase();
|
|
3559
3756
|
const presenceCtx = capturePackPresenceContext(cliWorkspace);
|
|
3757
|
+
// A disabled Manager cannot answer its own installed-list or queue routes.
|
|
3758
|
+
// For local enable, only permit the recovery path for a validated real
|
|
3759
|
+
// `<pack>.disabled` child of this operation's captured custom_nodes root.
|
|
3760
|
+
// Remote sessions never inspect or mutate local disk.
|
|
3761
|
+
const localPackId = !presenceCtx.remote ? assertSafeLocalPackId(id) : id;
|
|
3762
|
+
const localRecoveryAllowed = !presenceCtx.remote && opts.useCmCli !== false && isManagerSelfTarget(localPackId);
|
|
3763
|
+
const disabledOnDisk = enable && !presenceCtx.remote && opts.useCmCli !== false && presenceCtx.diskRoot
|
|
3764
|
+
? findDisabledPackOnDisk(localPackId, presenceCtx.diskRoot)
|
|
3765
|
+
: undefined;
|
|
3766
|
+
const managerDisabledOnDisk = localRecoveryAllowed &&
|
|
3767
|
+
disabledOnDisk &&
|
|
3768
|
+
isManagerSelfTarget(disabledOnDisk.cliId)
|
|
3769
|
+
? disabledOnDisk
|
|
3770
|
+
: undefined;
|
|
3560
3771
|
// CLI availability probe FIRST (#808 fallback discipline), but NO subprocess
|
|
3561
3772
|
// runs yet: the presence pre-check below comes before either mechanism,
|
|
3562
3773
|
// because a CLI run of an already-satisfied/no-such-pack request reports a
|
|
@@ -3572,13 +3783,29 @@ async function setCustomNodeEnabled(opts, enable) {
|
|
|
3572
3783
|
`comfy-cli was requested (useCmCli) but is not usable here: ${cliProblem}. ` +
|
|
3573
3784
|
`NOTHING was run through comfy-cli — ComfyUI-Manager was used for what follows instead.`;
|
|
3574
3785
|
}
|
|
3786
|
+
// Honor an explicitly requested usable comfy-cli BEFORE the Manager list
|
|
3787
|
+
// precheck. This is the only safe early escape: the directory was found by
|
|
3788
|
+
// enumerating the captured custom_nodes root, its name is validated, and the
|
|
3789
|
+
// CLI is given the canonical directory name rather than the raw caller id.
|
|
3790
|
+
if (enable && useCli && managerDisabledOnDisk && presenceCtx.diskRoot) {
|
|
3791
|
+
return enablePackViaComfyCliFromDisk(localPackId, presenceCtx.diskRoot, managerDisabledOnDisk);
|
|
3792
|
+
}
|
|
3575
3793
|
// Pre-op validation: a disable/enable for an id Manager never heard of is a
|
|
3576
3794
|
// silent no-op (queue or CLI alike), and the LEGACY dialects key the body on
|
|
3577
3795
|
// the pack's REAL installed version (node-bisect's controller does the same
|
|
3578
3796
|
// — "never send 'unknown' for an installed pack"). Both need the installed
|
|
3579
3797
|
// list read up front.
|
|
3580
3798
|
const presence = await resolvePackPresence(id, base, presenceCtx);
|
|
3799
|
+
if (enable &&
|
|
3800
|
+
disabledOnDisk &&
|
|
3801
|
+
!managerDisabledOnDisk &&
|
|
3802
|
+
(presence.state === "on-disk" || presence.state === "unverifiable")) {
|
|
3803
|
+
throw new NodeManagementError(`Refusing local recovery for "${id}": only the validated ` +
|
|
3804
|
+
`ComfyUI-Manager identity may be enabled from a .disabled directory ` +
|
|
3805
|
+
`without a readable Manager installed-pack list. NOTHING was changed.`);
|
|
3806
|
+
}
|
|
3581
3807
|
if (presence.state === "on-disk" &&
|
|
3808
|
+
!(enable && managerDisabledOnDisk) &&
|
|
3582
3809
|
// A READABLE list that doesn't track the pack: refuse — neither mechanism
|
|
3583
3810
|
// can verify the op through it. An UNREADABLE list with the pack on disk
|
|
3584
3811
|
// only blocks the HTTP path; comfy-cli works on the local install directly,
|
|
@@ -3595,6 +3822,16 @@ async function setCustomNodeEnabled(opts, enable) {
|
|
|
3595
3822
|
`Manager can ${op} it could NOT be determined and NOTHING was queued. Check ` +
|
|
3596
3823
|
`that ComfyUI-Manager is reachable, then retry.`);
|
|
3597
3824
|
}
|
|
3825
|
+
// Manager-unreadable or Manager-untracked local disabled packs can still be
|
|
3826
|
+
// enabled safely without queueing blind: the source and destination are
|
|
3827
|
+
// validated siblings under the captured custom_nodes root, and the rename is
|
|
3828
|
+
// followed by an exact on-disk postcondition check.
|
|
3829
|
+
if (enable &&
|
|
3830
|
+
managerDisabledOnDisk &&
|
|
3831
|
+
presenceCtx.diskRoot &&
|
|
3832
|
+
(presence.state === "on-disk" || presence.state === "unverifiable")) {
|
|
3833
|
+
return enablePackFromDisk(localPackId, presenceCtx.diskRoot, managerDisabledOnDisk);
|
|
3834
|
+
}
|
|
3598
3835
|
if (presence.state === "absent") {
|
|
3599
3836
|
throw new NodeManagementError(presence.evidence === "manager+disk"
|
|
3600
3837
|
? `"${id}" is not installed — it is in neither ComfyUI-Manager's installed-pack ` +
|