comfyui-mcp 0.52.76 → 0.52.78
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/orchestrator/panel-arg-aliases.js +1 -0
- package/dist/orchestrator/panel-arg-aliases.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +107 -21
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/manifest.js +67 -9
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/node-management.js +218 -51
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/workspace-env.js +8 -5
- package/dist/services/workspace-env.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,14 +2,14 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
-
import { config, getComfyUIBaseUrl, isRemoteMode } from "../config.js";
|
|
5
|
+
import { config, getComfyUIBaseUrl, getComfyuiTargetGeneration, isRemoteMode, } from "../config.js";
|
|
6
6
|
import { comfyuiFetch } from "../comfyui/fetch.js";
|
|
7
7
|
import { resetObjectInfoCache } from "../comfyui/client.js";
|
|
8
8
|
import { progressEnabled, reportDownloadProgress } from "./download-progress.js";
|
|
9
9
|
import { parsePyproject } from "./node-authoring.js";
|
|
10
10
|
import { getNodePackDetails } from "./registry-client.js";
|
|
11
11
|
import { cacheManagerApi, dialectRecheckSuppressed, getCachedManagerApi, managerApiCacheStamp, managerApiEpoch, resetManagerApiCache, setManagerApiCacheForTests, suppressDialectRecheck, } from "./manager-api-cache.js";
|
|
12
|
-
import { detectLocalWriteTargetMismatch, resolveEffectiveComfyUIBase, resolveEffectiveComfyUICodeBase, resolveInstallInterpreter, } from "./workspace-env.js";
|
|
12
|
+
import { detectLocalWriteTargetMismatch, resolveEffectiveComfyUIBase, resolveEffectiveComfyUICodeBase, resolveCustomNodesScanBaseLiveStrict, resolveInstallInterpreter, } from "./workspace-env.js";
|
|
13
13
|
import { assertComfyCliOk, getComfyCliVersion, isSupportedComfyCliVersion, resolveComfyCliExecutable, runComfyCliSync, } from "./comfy-cli.js";
|
|
14
14
|
import { withPanelPinGuard } from "./panel-pin-guard.js";
|
|
15
15
|
import { ambiguousBareNameRefusal, ambiguousBareNameWarning, bareNameAmbiguity, isFromSourceSpec, registryVersionAmbiguity, registryVersionRefusal, rekeyedRegistryVersionAmbiguity, rekeyedRegistryVersionRefusal, rekeyedRepoAmbiguity, rekeyedRepoRefusal, rekeyedRepoWarning, } from "./manager-bare-name-collisions.js";
|
|
@@ -156,7 +156,7 @@ export function explainManagerAuthenticationRequired(status) {
|
|
|
156
156
|
credentialHint);
|
|
157
157
|
}
|
|
158
158
|
async function managerFetch(path, options = {}) {
|
|
159
|
-
const { method = "GET", body, base = managerBaseUrl(), soft = false } = options;
|
|
159
|
+
const { method = "GET", body, base = managerBaseUrl(), soft = false, onSoftFailure, } = options;
|
|
160
160
|
const url = `${base}${path}`;
|
|
161
161
|
logger.debug("Manager API request", { url, method });
|
|
162
162
|
const headers = {};
|
|
@@ -171,8 +171,10 @@ async function managerFetch(path, options = {}) {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
catch (err) {
|
|
174
|
-
if (soft)
|
|
174
|
+
if (soft) {
|
|
175
|
+
onSoftFailure?.(undefined);
|
|
175
176
|
return undefined;
|
|
177
|
+
}
|
|
176
178
|
throw new NodeManagementError(`ComfyUI-Manager API unreachable at ${url}. Is ComfyUI running with ComfyUI-Manager installed? (${err instanceof Error ? err.message : String(err)})`);
|
|
177
179
|
}
|
|
178
180
|
if (!res.ok) {
|
|
@@ -181,6 +183,7 @@ async function managerFetch(path, options = {}) {
|
|
|
181
183
|
// text, never a wrong conclusion. Verified there is no branch on this value.
|
|
182
184
|
const text = await res.text().catch(() => "");
|
|
183
185
|
if (soft) {
|
|
186
|
+
onSoftFailure?.(res.status);
|
|
184
187
|
// A soft probe may ignore an absent route or a transient server error, but
|
|
185
188
|
// authentication is actionable evidence. Returning undefined here made
|
|
186
189
|
// detectManagerApi() fall through to its false "Manager is missing"
|
|
@@ -331,6 +334,8 @@ function looksLikeQueueStatus(s) {
|
|
|
331
334
|
typeof q.pending_count === "number" ||
|
|
332
335
|
typeof q.done_count === "number");
|
|
333
336
|
}
|
|
337
|
+
/** Internal marker carried by the all-soft queue-generation probe. */
|
|
338
|
+
const MANAGER_QUEUE_DETECTION_FAILURE = "manager-queue-detection";
|
|
334
339
|
/**
|
|
335
340
|
* Probe both queue/status dialects while preserving their HTTP status. This is
|
|
336
341
|
* intentionally separate from `managerFetch(..., { soft: true })`, whose
|
|
@@ -487,13 +492,26 @@ async function probeManagerApi(base) {
|
|
|
487
492
|
// conclusion describes a server that may be gone and must not be pinned over
|
|
488
493
|
// the newer state (#646).
|
|
489
494
|
const stamp = managerApiCacheStamp();
|
|
490
|
-
const
|
|
495
|
+
const queueStatusCodes = [undefined, undefined];
|
|
496
|
+
const v2 = await managerFetch("/v2/manager/queue/status", {
|
|
497
|
+
base,
|
|
498
|
+
soft: true,
|
|
499
|
+
onSoftFailure: (status) => {
|
|
500
|
+
queueStatusCodes[0] = status;
|
|
501
|
+
},
|
|
502
|
+
});
|
|
491
503
|
if (looksLikeQueueStatus(v2)) {
|
|
492
504
|
const api = await resolveV2SubDialect(base);
|
|
493
505
|
cacheManagerApi(base, api, stamp);
|
|
494
506
|
return api;
|
|
495
507
|
}
|
|
496
|
-
const legacy = await managerFetch("/manager/queue/status", {
|
|
508
|
+
const legacy = await managerFetch("/manager/queue/status", {
|
|
509
|
+
base,
|
|
510
|
+
soft: true,
|
|
511
|
+
onSoftFailure: (status) => {
|
|
512
|
+
queueStatusCodes[1] = status;
|
|
513
|
+
},
|
|
514
|
+
});
|
|
497
515
|
if (looksLikeQueueStatus(legacy)) {
|
|
498
516
|
// /manager/queue/status answering ALMOST always means the released 3.x
|
|
499
517
|
// custom-node Manager — but do NOT brand it "legacy 3.x" (and speak 3.x
|
|
@@ -526,7 +544,11 @@ async function probeManagerApi(base) {
|
|
|
526
544
|
throw new NodeManagementError("ComfyUI-Manager's queue API is not reachable (neither /v2/manager/queue/status " +
|
|
527
545
|
"nor /manager/queue/status answered with a queue status). Is ComfyUI-Manager " +
|
|
528
546
|
"installed and enabled on the connected ComfyUI? The pip comfyui_manager " +
|
|
529
|
-
"package only activates when ComfyUI is started with --enable-manager."
|
|
547
|
+
"package only activates when ComfyUI is started with --enable-manager.", {
|
|
548
|
+
kind: MANAGER_QUEUE_DETECTION_FAILURE,
|
|
549
|
+
base,
|
|
550
|
+
queueStatusCodes,
|
|
551
|
+
});
|
|
530
552
|
}
|
|
531
553
|
/** Queue route prefix for a dialect ("v2-batch" serves status/start under the
|
|
532
554
|
* /v2 prefix too — only the mutation routes differ). */
|
|
@@ -1859,12 +1881,13 @@ export function findPackOnDisk(id, comfyuiBase) {
|
|
|
1859
1881
|
}
|
|
1860
1882
|
return { state: "not-found", scanned: customNodes };
|
|
1861
1883
|
}
|
|
1862
|
-
|
|
1863
|
-
export function capturePackPresenceContext(diskRoot) {
|
|
1884
|
+
export function capturePackPresenceContext(diskRoot, options = {}) {
|
|
1864
1885
|
const remote = isRemoteMode();
|
|
1865
1886
|
return {
|
|
1866
1887
|
remote,
|
|
1867
|
-
diskRoot: remote
|
|
1888
|
+
diskRoot: remote || (diskRoot === undefined && options.allowConfiguredFallback === false)
|
|
1889
|
+
? undefined
|
|
1890
|
+
: (diskRoot ?? resolveEffectiveComfyUIBase()),
|
|
1868
1891
|
};
|
|
1869
1892
|
}
|
|
1870
1893
|
/**
|
|
@@ -2154,16 +2177,21 @@ function discardFailedClone(nodeDir, alreadyPresent, settlingProcesses = false)
|
|
|
2154
2177
|
`import it on every start and log an error — delete that directory by hand.`);
|
|
2155
2178
|
}
|
|
2156
2179
|
/**
|
|
2157
|
-
* Did ComfyUI-Manager
|
|
2180
|
+
* Did ComfyUI-Manager reject the route outright, before queueing anything?
|
|
2158
2181
|
*
|
|
2159
|
-
* A git-URL
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2182
|
+
* A legacy 3.x git-URL route can be absent, and a Manager-native install can be
|
|
2183
|
+
* refused before queueing by its `security_level` / `allow_git_url_install`
|
|
2184
|
+
* policy. Both arrive as a status on the POST itself:
|
|
2162
2185
|
*
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2186
|
+
* 404 — the route is not registered on this build. Git installs use
|
|
2187
|
+
* `/manager/queue/install`, `/v2/manager/queue/task`, or the legacy-UI
|
|
2188
|
+
* `/v2/manager/queue/batch` envelope.
|
|
2165
2189
|
*
|
|
2166
|
-
*
|
|
2190
|
+
* 403 is admitted only when Manager's authoritative JSON error shape names its
|
|
2191
|
+
* own policy gate, or when one of the exact legacy Manager policy responses is
|
|
2192
|
+
* present. A bare 403 may be an authentication/permission layer and stays
|
|
2193
|
+
* fail-closed. 405 is deliberately
|
|
2194
|
+
* NOT here. On this API it is the DIALECT-MISMATCH signal
|
|
2167
2195
|
* (ComfyUI's frontend catchall answering a route registered under the other
|
|
2168
2196
|
* generation), and the self-heal retry in #646 already owns it — diverting to a
|
|
2169
2197
|
* clone would pre-empt a Manager install that is about to succeed on the right
|
|
@@ -2175,16 +2203,100 @@ function discardFailedClone(nodeDir, alreadyPresent, settlingProcesses = false)
|
|
|
2175
2203
|
* That is the whole warrant for continuing — and it is why 5xx is excluded, where
|
|
2176
2204
|
* a task may well have been accepted before the handler fell over.
|
|
2177
2205
|
*
|
|
2178
|
-
* Returns
|
|
2179
|
-
*
|
|
2206
|
+
* Returns 404, or the explicitly Manager-owned policy 403, or undefined when the
|
|
2207
|
+
* failure is anything else (including 401, bare 403, 407, a transport error, a
|
|
2208
|
+
* timeout, or a 500) and must keep propagating.
|
|
2209
|
+
*/
|
|
2210
|
+
const LEGACY_MANAGER_SECURITY_ERROR = "A security error has occurred. Please check the terminal logs";
|
|
2211
|
+
/**
|
|
2212
|
+
* Keep the old plain-text Manager response working without treating arbitrary
|
|
2213
|
+
* response text as proof of Manager ownership. JSON is the current authoritative
|
|
2214
|
+
* Manager shape; the two exact strings are retained for the legacy response and
|
|
2215
|
+
* the existing local compatibility fixture.
|
|
2180
2216
|
*/
|
|
2217
|
+
function isManagerOwnedGitPolicy403(body) {
|
|
2218
|
+
if (typeof body !== "string")
|
|
2219
|
+
return false;
|
|
2220
|
+
const text = body.trim();
|
|
2221
|
+
if (text === LEGACY_MANAGER_SECURITY_ERROR ||
|
|
2222
|
+
text === "gated by security_level") {
|
|
2223
|
+
return true;
|
|
2224
|
+
}
|
|
2225
|
+
try {
|
|
2226
|
+
const parsed = JSON.parse(text);
|
|
2227
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2228
|
+
return false;
|
|
2229
|
+
const reason = parsed.error;
|
|
2230
|
+
return reason === "security_level" || reason === "allow_git_url_install";
|
|
2231
|
+
}
|
|
2232
|
+
catch {
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2181
2236
|
function managerEnqueueRefusal(err) {
|
|
2182
2237
|
if (!(err instanceof NodeManagementError))
|
|
2183
2238
|
return undefined;
|
|
2184
|
-
const
|
|
2239
|
+
const details = err.details;
|
|
2240
|
+
const status = details?.status;
|
|
2241
|
+
const url = details?.url;
|
|
2185
2242
|
if (typeof status !== "number")
|
|
2186
2243
|
return undefined;
|
|
2187
|
-
|
|
2244
|
+
if (typeof url !== "string" ||
|
|
2245
|
+
!/\/manager\/queue\/(?:task|install|batch)(?:[?#]|$)/.test(url)) {
|
|
2246
|
+
return undefined;
|
|
2247
|
+
}
|
|
2248
|
+
if (status === 404)
|
|
2249
|
+
return status;
|
|
2250
|
+
if (status === 403 &&
|
|
2251
|
+
isManagerOwnedGitPolicy403(details?.body)) {
|
|
2252
|
+
return status;
|
|
2253
|
+
}
|
|
2254
|
+
return undefined;
|
|
2255
|
+
}
|
|
2256
|
+
function isManagerQueueDetectionFailure(err) {
|
|
2257
|
+
if (!(err instanceof NodeManagementError) || !err.details || typeof err.details !== "object") {
|
|
2258
|
+
return false;
|
|
2259
|
+
}
|
|
2260
|
+
return err.details.kind === MANAGER_QUEUE_DETECTION_FAILURE;
|
|
2261
|
+
}
|
|
2262
|
+
function initialManagerQueueAbsenceWasProven(err) {
|
|
2263
|
+
if (!(err instanceof NodeManagementError) || !err.details || typeof err.details !== "object") {
|
|
2264
|
+
return false;
|
|
2265
|
+
}
|
|
2266
|
+
const codes = err.details.queueStatusCodes;
|
|
2267
|
+
return (Array.isArray(codes) &&
|
|
2268
|
+
codes.length === 2 &&
|
|
2269
|
+
codes.every((status) => status === 404));
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* A direct clone is safe after either a Manager-native policy refusal whose body
|
|
2273
|
+
* explicitly proves no task was queued, a direct enqueue route-level 404, or a
|
|
2274
|
+
* detection failure followed by a fresh probe proving BOTH queue dialects are
|
|
2275
|
+
* 404 on the same captured ComfyUI base.
|
|
2276
|
+
* A bare 401/403/407, timeout, 5xx, malformed response, or unrelated reachability
|
|
2277
|
+
* error remains fail-closed.
|
|
2278
|
+
*/
|
|
2279
|
+
async function managerAbsenceAllowsGitFallback(err, base) {
|
|
2280
|
+
const status = errorStatus(err);
|
|
2281
|
+
const preQueueRefusal = managerEnqueueRefusal(err);
|
|
2282
|
+
// The enqueue route's own 404 is already a pre-queue refusal. Preserve the
|
|
2283
|
+
// legacy contract that authorized a direct clone on that route-level fact;
|
|
2284
|
+
// the fresh status probes are required only for the new detection-failure
|
|
2285
|
+
// path, where the enqueue error carries no route-level refusal.
|
|
2286
|
+
if (preQueueRefusal === 403 || preQueueRefusal === 404)
|
|
2287
|
+
return true;
|
|
2288
|
+
if (status !== undefined)
|
|
2289
|
+
return false;
|
|
2290
|
+
if (status === undefined &&
|
|
2291
|
+
(!isManagerQueueDetectionFailure(err) || !initialManagerQueueAbsenceWasProven(err))) {
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
try {
|
|
2295
|
+
return (await probeManagerQueueAvailability(base)) === "absent";
|
|
2296
|
+
}
|
|
2297
|
+
catch {
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2188
2300
|
}
|
|
2189
2301
|
/**
|
|
2190
2302
|
* #1765 (recurrence on 0.52.35) — the write was aimed at the wrong ComfyUI.
|
|
@@ -2241,11 +2353,11 @@ async function cloneCustomNodeFallback(gitId, repoName, gitRef, managerStatus, b
|
|
|
2241
2353
|
*/
|
|
2242
2354
|
opts) {
|
|
2243
2355
|
const because = opts?.managerRefusalNote ?? `"${repoName}" is not in the ComfyUI-Manager registry`;
|
|
2244
|
-
// basePath is the CALL-SCOPED local ComfyUI root
|
|
2245
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
|
|
2248
|
-
|
|
2356
|
+
// basePath is the CALL-SCOPED local ComfyUI root. When the caller has already
|
|
2357
|
+
// established that no safe root exists, do not re-read the saved-default
|
|
2358
|
+
// resolver and recreate the wrong-machine clone hazard.
|
|
2359
|
+
const comfyuiBase = basePath ??
|
|
2360
|
+
(opts?.allowSharedWorkspaceFallback === false ? undefined : resolveEffectiveComfyUIBase());
|
|
2249
2361
|
// Same hazard as the CLI paths (codex gate P0): the guard below catches "no
|
|
2250
2362
|
// path", but the dangerous case is HAVING one while connected elsewhere. A
|
|
2251
2363
|
// clone into a stale local tree would report a successful install of a pack the
|
|
@@ -2493,6 +2605,37 @@ function comfyCliUnavailableReason(workspace) {
|
|
|
2493
2605
|
}
|
|
2494
2606
|
return undefined;
|
|
2495
2607
|
}
|
|
2608
|
+
async function resolveInstallLocalWorkspace(opts) {
|
|
2609
|
+
if (opts.comfyuiPath !== undefined)
|
|
2610
|
+
return opts.comfyuiPath;
|
|
2611
|
+
if (opts.localCloneFallback === "verified-only")
|
|
2612
|
+
return undefined;
|
|
2613
|
+
// An unset COMFYUI_PATH is not permission to use the saved default. The
|
|
2614
|
+
// connected local server must identify a usable custom_nodes scan root, or
|
|
2615
|
+
// this operation remains Manager-only. This resolver never throws for an
|
|
2616
|
+
// unavailable/ambiguous live root; the Manager attempt still proceeds.
|
|
2617
|
+
if (!process.env.COMFYUI_PATH && !isRemoteMode()) {
|
|
2618
|
+
try {
|
|
2619
|
+
return await resolveCustomNodesScanBaseLiveStrict({ requireLive: true });
|
|
2620
|
+
}
|
|
2621
|
+
catch {
|
|
2622
|
+
return undefined;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return resolveEffectiveComfyUIBase();
|
|
2626
|
+
}
|
|
2627
|
+
function assertInstallTargetStable(expectedGeneration, managerBase) {
|
|
2628
|
+
const actualGeneration = getComfyuiTargetGeneration();
|
|
2629
|
+
if (actualGeneration === expectedGeneration)
|
|
2630
|
+
return;
|
|
2631
|
+
throw new ProcessControlError("ComfyUI's target changed while this custom-node install was being prepared. " +
|
|
2632
|
+
"The Manager request and any local clone were refused; retry after the target settles.", {
|
|
2633
|
+
kind: "target-generation-changed",
|
|
2634
|
+
managerBase,
|
|
2635
|
+
expectedGeneration,
|
|
2636
|
+
actualGeneration,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2496
2639
|
async function installCustomNodeImpl(opts) {
|
|
2497
2640
|
const { id, version, mode = "remote", channel = "default" } = opts;
|
|
2498
2641
|
const parsedGit = parseGitUrl(id);
|
|
@@ -2523,31 +2666,32 @@ async function installCustomNodeImpl(opts) {
|
|
|
2523
2666
|
if (source === "git")
|
|
2524
2667
|
assertSafeGitUrl(gitId);
|
|
2525
2668
|
// Keep the mutation, its post-queue verification, AND any local filesystem
|
|
2526
|
-
// work on the target selected for this invocation
|
|
2527
|
-
//
|
|
2528
|
-
//
|
|
2529
|
-
//
|
|
2530
|
-
//
|
|
2531
|
-
//
|
|
2532
|
-
|
|
2533
|
-
const
|
|
2534
|
-
const cliWorkspace = opts.
|
|
2535
|
-
|
|
2669
|
+
// work on the target selected for this invocation. The Manager base is
|
|
2670
|
+
// captured before the live-root lookup so a panel retarget cannot move the
|
|
2671
|
+
// HTTP half of this operation while the filesystem target is being verified.
|
|
2672
|
+
// With COMFYUI_PATH unset, a live custom_nodes root is required; a saved
|
|
2673
|
+
// default is machine-local configuration, not proof of what this panel target
|
|
2674
|
+
// scans.
|
|
2675
|
+
const managerBase = opts.managerBase ?? managerBaseUrl();
|
|
2676
|
+
const targetGeneration = opts.targetGeneration ?? getComfyuiTargetGeneration();
|
|
2677
|
+
const cliWorkspace = source === "git" || opts.useCmCli
|
|
2678
|
+
? await resolveInstallLocalWorkspace(opts)
|
|
2679
|
+
: opts.comfyuiPath ?? resolveEffectiveComfyUIBase();
|
|
2680
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2681
|
+
const presenceCtx = capturePackPresenceContext(cliWorkspace, {
|
|
2682
|
+
allowConfiguredFallback: cliWorkspace !== undefined,
|
|
2683
|
+
});
|
|
2536
2684
|
// #1765 — and is that root the install this session is actually CONNECTED to?
|
|
2537
2685
|
// Pinned to the very cliWorkspace the write will use, so a panel retarget
|
|
2538
2686
|
// mid-call cannot judge one install's root against another's server.
|
|
2539
2687
|
//
|
|
2540
|
-
// AFTER presenceCtx, deliberately: this is the first await in the function, and
|
|
2541
|
-
// capturePackPresenceContext must be taken at operation ENTRY from the mode and
|
|
2542
|
-
// config as they are NOW (codex gate round 11 — computing it after an await lets
|
|
2543
|
-
// a mid-op retarget pair Manager A's answer with disk B).
|
|
2544
|
-
//
|
|
2545
2688
|
// Taken only when a LOCAL write is reachable for this call: a registry install
|
|
2546
2689
|
// goes through ComfyUI-Manager, which targets the connected server by
|
|
2547
2690
|
// construction and has no local destination to misdirect.
|
|
2548
2691
|
const localWriteMismatch = source === "git" || opts.useCmCli === true
|
|
2549
2692
|
? await detectLocalWriteTargetMismatch(cliWorkspace)
|
|
2550
2693
|
: undefined;
|
|
2694
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2551
2695
|
// THE PRE-STATE, OBSERVED BEFORE THE OPERATION (codex gate P0). The "already
|
|
2552
2696
|
// installed" verdict below used to be inferred from POST-op disk state alone:
|
|
2553
2697
|
// a pack that was absent before the call and installed as a registry ZIP —
|
|
@@ -2655,28 +2799,44 @@ async function installCustomNodeImpl(opts) {
|
|
|
2655
2799
|
// A remote target keeps the original error: the clone would write to a
|
|
2656
2800
|
// LOCAL tree that is not the server we are connected to, so "the Manager
|
|
2657
2801
|
// refused" is the honest and complete answer there.
|
|
2658
|
-
|
|
2802
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2803
|
+
const absenceAllows = await managerAbsenceAllowsGitFallback(err, managerBase);
|
|
2804
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2805
|
+
if (isRemoteMode() || !absenceAllows) {
|
|
2659
2806
|
throw err;
|
|
2807
|
+
}
|
|
2660
2808
|
// BEFORE the log line, which says "cloning directly" and would otherwise be
|
|
2661
2809
|
// recording an action that then does not happen.
|
|
2662
2810
|
if (localWriteMismatch) {
|
|
2663
2811
|
throw new ProcessControlError(wrongInstallRefusal(localWriteMismatch, 'install_custom_node (action:"install", git clone)', managerBase));
|
|
2664
2812
|
}
|
|
2665
|
-
logger.info("Manager refused
|
|
2813
|
+
logger.info("Manager refused or was proven absent — cloning directly", {
|
|
2666
2814
|
status: refusedBy,
|
|
2667
2815
|
gitId,
|
|
2668
2816
|
});
|
|
2669
|
-
|
|
2817
|
+
const cloned = await cloneCustomNodeFallback(gitId, repoName, gitRef, refusedBy === 403 || refusedBy === 404
|
|
2818
|
+
? { manager_refused: refusedBy }
|
|
2819
|
+
: { manager_absent: true }, cliWorkspace, {
|
|
2670
2820
|
refFromVersion,
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
`
|
|
2674
|
-
|
|
2821
|
+
allowSharedWorkspaceFallback: cliWorkspace !== undefined,
|
|
2822
|
+
managerRefusalNote: refusedBy === 403
|
|
2823
|
+
? `ComfyUI-Manager REFUSED the git-URL install (HTTP 403) via its ` +
|
|
2824
|
+
`security_level / allow_git_url_install policy gate. Nothing was queued there.`
|
|
2825
|
+
: refusedBy === 404
|
|
2826
|
+
? `ComfyUI-Manager's git-install route returned HTTP 404 before queueing. ` +
|
|
2827
|
+
`Nothing was queued there.`
|
|
2828
|
+
: `ComfyUI-Manager is confirmed absent: both queue/status dialects returned HTTP 404 ` +
|
|
2829
|
+
`on the connected ComfyUI. Nothing was queued there.`,
|
|
2830
|
+
});
|
|
2831
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2832
|
+
return withCliNote(cloned);
|
|
2675
2833
|
}
|
|
2834
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2676
2835
|
// VERIFY: /v2/customnode/installed reflects on-disk custom_nodes, so a
|
|
2677
2836
|
// freshly-cloned pack shows up even before a reboot. If the Manager actually
|
|
2678
2837
|
// installed it, we're done; otherwise it's unregistered → clone it directly.
|
|
2679
2838
|
const installed = await listInstalledNodesAt(managerBase).catch(() => []);
|
|
2839
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2680
2840
|
if (nodeInstalledMatches(gitId, installed)) {
|
|
2681
2841
|
return withCliNote({
|
|
2682
2842
|
mechanism: "manager-http",
|
|
@@ -2687,7 +2847,12 @@ async function installCustomNodeImpl(opts) {
|
|
|
2687
2847
|
if (localWriteMismatch) {
|
|
2688
2848
|
throw new ProcessControlError(wrongInstallRefusal(localWriteMismatch, 'install_custom_node (action:"install", git clone)', managerBase));
|
|
2689
2849
|
}
|
|
2690
|
-
|
|
2850
|
+
const cloned = await cloneCustomNodeFallback(gitId, repoName, gitRef, status, cliWorkspace, {
|
|
2851
|
+
refFromVersion,
|
|
2852
|
+
allowSharedWorkspaceFallback: cliWorkspace !== undefined,
|
|
2853
|
+
});
|
|
2854
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2855
|
+
return withCliNote(cloned);
|
|
2691
2856
|
}
|
|
2692
2857
|
// Registry (plain CNR id). Keep the prior defaults channel "default" /
|
|
2693
2858
|
// mode "remote" (overridable via opts) — forcing "dev"/"cache" risks resolving
|
|
@@ -2702,10 +2867,12 @@ async function installCustomNodeImpl(opts) {
|
|
|
2702
2867
|
channel,
|
|
2703
2868
|
mode,
|
|
2704
2869
|
}, managerBase);
|
|
2870
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2705
2871
|
// #797: verify across BOTH sources (Manager list + local disk) and keep
|
|
2706
2872
|
// "could not determine" distinct from "determined absent" — an unreadable
|
|
2707
2873
|
// Manager list collapsed to [] used to read as "not found in the registry".
|
|
2708
2874
|
const presence = await resolvePackPresence(id, managerBase, presenceCtx);
|
|
2875
|
+
assertInstallTargetStable(targetGeneration, managerBase);
|
|
2709
2876
|
switch (presence.state) {
|
|
2710
2877
|
case "manager-listed":
|
|
2711
2878
|
return withCliNote({
|
|
@@ -4254,8 +4421,8 @@ async function listInstalledNodesAt(base, opts = {}) {
|
|
|
4254
4421
|
}
|
|
4255
4422
|
return parseInstalled(raw);
|
|
4256
4423
|
}
|
|
4257
|
-
export async function listInstalledNodes(opts = {}) {
|
|
4258
|
-
return listInstalledNodesAt(
|
|
4424
|
+
export async function listInstalledNodes(opts = {}, base = managerBaseUrl()) {
|
|
4425
|
+
return listInstalledNodesAt(base, opts);
|
|
4259
4426
|
}
|
|
4260
4427
|
/**
|
|
4261
4428
|
* Reconcile installed-node Python dependencies. comfy-cli exposes this as
|