Skip to content

Commit 7c77553

Browse files
authored
Merge pull request #20 from Kosinkadink/update-support
Update Support: Commit-based, Copy, Migration, and Release-based Updates for Standalone
2 parents a58d18c + 78c457a commit 7c77553

22 files changed

Lines changed: 2396 additions & 209 deletions

.github/workflows/build-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- name: Setup Node.js
3636
uses: actions/setup-node@v4
3737
with:
38-
node-version: 20
38+
node-version: 22
3939
cache: npm
4040

4141
- name: Set version from tag

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ chmod +x ComfyUI-Launcher-*.AppImage
3636

3737
### Prerequisites
3838

39-
- [Node.js](https://nodejs.org/) (v18+)
39+
- [Node.js](https://nodejs.org/) **v22 LTS** or later
40+
41+
We recommend using [nvm](https://github.com/nvm-sh/nvm) (or [nvm-windows](https://github.com/coreybutler/nvm-windows)) to manage Node versions:
42+
43+
```bash
44+
# Install and use Node 22
45+
nvm install 22
46+
nvm use 22
47+
48+
# Verify
49+
node --version # should print v22.x.x
50+
```
4051

4152
### Setup
4253

docs/migration-parity.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Migration Feature Parity with ComfyUI-Manager v4
2+
3+
## Current Implementation (Step 3)
4+
5+
Our migration copies custom node directories and installs `requirements.txt`
6+
via `uv pip install`, filtering out PyTorch packages. This covers the basic
7+
case but misses several things Manager does.
8+
9+
## What We're Missing
10+
11+
### 1. `install.py` Execution (High Priority)
12+
13+
Many custom nodes ship an `install.py` that runs arbitrary setup: downloading
14+
models, compiling C/CUDA extensions, creating config files, etc. Manager runs
15+
this **after** `requirements.txt` with `cwd` set to the node directory and two
16+
environment variables:
17+
18+
- `COMFYUI_PATH` — path to the ComfyUI root
19+
- `COMFYUI_FOLDERS_BASE_PATH` — base path for ComfyUI folders
20+
21+
### 2. pip Blacklist / Downgrade Protection (Medium Priority)
22+
23+
Manager maintains:
24+
25+
- **Blacklist** (never install): `torch`, `torchaudio`, `torchsde`,
26+
`torchvision`
27+
- **Downgrade blacklist** (never downgrade): `transformers`, `safetensors`,
28+
`kornia`, plus all of the above
29+
30+
We currently only filter `torch`, `torchvision`, `torchaudio` from
31+
requirements. Missing `torchsde`. No downgrade protection.
32+
33+
### 3. `--index-url` Parsing (Low Priority)
34+
35+
Manager parses `--index-url` / `--extra-index-url` directives inline in
36+
`requirements.txt` and forwards them to pip. We currently pass the entire
37+
filtered file to `uv pip install` which may not handle these correctly.
38+
39+
### 4. `pip_fixer.fix_broken()` (Low Priority)
40+
41+
After installing requirements, Manager runs a broken-package repair pass.
42+
We don't do this.
43+
44+
### 5. `pip_overrides.json` (Low Priority)
45+
46+
Manager supports user-configured package name remapping. Not relevant for
47+
migration but worth noting for completeness.
48+
49+
## Delegating to ComfyUI-Manager
50+
51+
If Manager is installed in the **destination** installation, we can delegate
52+
the entire post-copy dependency process to it instead of doing it ourselves.
53+
This gets us full feature parity for free.
54+
55+
### Option A: `cm-cli.py restore-dependencies` (Recommended)
56+
57+
Run after copying node directories, before the user launches ComfyUI:
58+
59+
```
60+
python <ComfyUI>/custom_nodes/ComfyUI-Manager/cm-cli.py restore-dependencies
61+
```
62+
63+
- Iterates every non-disabled directory in `custom_nodes/`
64+
- Calls `execute_install_script` for each (requirements + install.py)
65+
- No running server needed, no knowledge of node IDs required
66+
- Works for all node types (git, CNR, unknown)
67+
68+
### Option B: `cm-cli.py post-install <path>`
69+
70+
For a single node:
71+
72+
```
73+
python <ComfyUI>/custom_nodes/ComfyUI-Manager/cm-cli.py post-install /path/to/node
74+
```
75+
76+
### Option C: Write `#LAZY-INSTALL-SCRIPT` entries
77+
78+
Append entries to `<user_dir>/ComfyUI-Manager/startup-scripts/install-scripts.txt`:
79+
80+
```python
81+
['/path/to/custom_nodes/MyNode', '#LAZY-INSTALL-SCRIPT', '/path/to/python']
82+
```
83+
84+
Manager processes these on next boot, then auto-restarts ComfyUI. This is the
85+
deferred approach — no extra subprocess needed during migration, but deps
86+
aren't installed until next launch.
87+
88+
### Detection
89+
90+
Check if Manager is present:
91+
92+
```javascript
93+
const managerCli = path.join(comfyUIDir, "custom_nodes", "ComfyUI-Manager", "cm-cli.py");
94+
const hasManager = fs.existsSync(managerCli);
95+
```
96+
97+
### Hybrid Strategy
98+
99+
1. Check if Manager exists in the destination installation
100+
2. If yes: use Option A (`restore-dependencies`) or Option C (lazy scripts)
101+
and skip our own `uv pip install` phase entirely
102+
3. If no: fall back to our current `uv pip install` approach (sans install.py)
103+
104+
Option C (lazy scripts) is attractive because:
105+
- Zero extra time during migration
106+
- Manager handles everything on next boot including restart
107+
- Consistent with how Manager itself queues installs on Windows
108+
109+
Option A is better when:
110+
- User wants deps ready before first launch
111+
- We want to show progress/output during migration
112+
113+
## TODO
114+
115+
- [x] Add `torchsde` to our PyTorch filter regex
116+
- [ ] Add `install.py` execution to our fallback (no-Manager) path
117+
- [ ] Detect Manager in destination and delegate when available
118+
- [ ] Add `--index-url` passthrough for requirements files

installations.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ async function list() {
2121
return load();
2222
}
2323

24+
function uniqueName(baseName, existing, excludeId) {
25+
const names = new Set(existing.filter((i) => i.id !== excludeId).map((i) => i.name));
26+
if (!names.has(baseName)) return baseName;
27+
let suffix = 2;
28+
while (names.has(`${baseName} (${suffix})`)) suffix++;
29+
return `${baseName} (${suffix})`;
30+
}
31+
2432
async function add(installation) {
2533
const installations = await load();
34+
installation.name = uniqueName(installation.name, installations);
2635
const entry = {
2736
id: `inst-${Date.now()}`,
2837
createdAt: new Date().toISOString(),
@@ -76,4 +85,4 @@ async function seedDefaults(defaults) {
7685
if (installations.length > 0) await save(installations);
7786
}
7887

79-
module.exports = { list, add, remove, update, get, reorder, seedDefaults };
88+
module.exports = { list, add, remove, update, get, reorder, seedDefaults, uniqueName };

lib/comfyui-releases.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { fetchJSON } = require("./fetch");
2+
3+
async function fetchLatestRelease(track) {
4+
if (track === "latest") {
5+
const REPO = "Comfy-Org/ComfyUI";
6+
const [commit, releases] = await Promise.all([
7+
fetchJSON(`https://api.github.com/repos/${REPO}/commits/master`),
8+
fetchJSON(`https://api.github.com/repos/${REPO}/releases?per_page=10`).catch(() => []),
9+
]);
10+
if (!commit) return null;
11+
const sha = commit.sha.slice(0, 7);
12+
const date = commit.commit?.committer?.date;
13+
const msg = commit.commit?.message?.split("\n")[0] || "";
14+
const stable = releases.find((r) => !r.draft && !r.prerelease);
15+
let label = sha;
16+
if (stable) {
17+
try {
18+
const cmp = await fetchJSON(`https://api.github.com/repos/${REPO}/compare/${stable.tag_name}...master`);
19+
const ahead = cmp.ahead_by;
20+
label = ahead > 0
21+
? `${stable.tag_name} + ${ahead} commit${ahead !== 1 ? "s" : ""} (${sha})`
22+
: stable.tag_name;
23+
} catch {
24+
label = `${stable.tag_name}+ (${sha})`;
25+
}
26+
}
27+
return {
28+
tag_name: sha,
29+
name: label,
30+
body: msg || "",
31+
html_url: commit.html_url,
32+
published_at: date,
33+
_commit: true,
34+
};
35+
}
36+
const releases = await fetchJSON(
37+
"https://api.github.com/repos/Comfy-Org/ComfyUI/releases?per_page=30"
38+
);
39+
return releases.find((r) => !r.draft && !r.prerelease) || null;
40+
}
41+
42+
function truncateNotes(text, maxLen) {
43+
if (!text) return "";
44+
if (text.length <= maxLen) return text;
45+
return text.slice(0, maxLen) + "\n\n… (truncated)";
46+
}
47+
48+
module.exports = { fetchLatestRelease, truncateNotes };

lib/copy.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
async function collectFiles(dir) {
5+
const files = [];
6+
const symlinks = [];
7+
const stack = [dir];
8+
while (stack.length > 0) {
9+
const current = stack.pop();
10+
const items = await fs.promises.readdir(current, { withFileTypes: true });
11+
for (const item of items) {
12+
const full = path.join(current, item.name);
13+
if (item.isSymbolicLink()) {
14+
symlinks.push(path.relative(dir, full));
15+
} else if (item.isDirectory()) {
16+
stack.push(full);
17+
} else {
18+
files.push(path.relative(dir, full));
19+
}
20+
}
21+
}
22+
return { files, symlinks };
23+
}
24+
25+
async function copyDirWithProgress(src, dest, onProgress, { signal } = {}) {
26+
const { files, symlinks } = await collectFiles(src);
27+
const total = files.length + symlinks.length;
28+
let copied = 0;
29+
const step = Math.max(1, Math.floor(total / 100));
30+
const concurrency = 50;
31+
const dirPromises = new Map();
32+
const startTime = Date.now();
33+
34+
const ensureDir = (dir) => {
35+
if (dirPromises.has(dir)) return dirPromises.get(dir);
36+
const p = fs.promises.mkdir(dir, { recursive: true });
37+
dirPromises.set(dir, p);
38+
return p;
39+
};
40+
41+
const reportProgress = () => {
42+
if (onProgress && (copied % step === 0 || copied === total)) {
43+
const elapsedSecs = (Date.now() - startTime) / 1000;
44+
const etaSecs = copied > 0 ? elapsedSecs * ((total - copied) / copied) : -1;
45+
onProgress(copied, total, elapsedSecs, etaSecs);
46+
}
47+
};
48+
49+
let i = 0;
50+
while (i < files.length) {
51+
if (signal?.aborted) throw new Error("Cancelled");
52+
const batch = files.slice(i, i + concurrency);
53+
await Promise.all(batch.map(async (rel) => {
54+
const destPath = path.join(dest, rel);
55+
await ensureDir(path.dirname(destPath));
56+
await fs.promises.copyFile(path.join(src, rel), destPath);
57+
copied++;
58+
reportProgress();
59+
}));
60+
i += concurrency;
61+
}
62+
63+
// Recreate symlinks, rewriting absolute targets that point inside src
64+
for (const rel of symlinks) {
65+
if (signal?.aborted) throw new Error("Cancelled");
66+
const srcLink = path.join(src, rel);
67+
const destLink = path.join(dest, rel);
68+
await ensureDir(path.dirname(destLink));
69+
let target = await fs.promises.readlink(srcLink);
70+
if (path.isAbsolute(target)) {
71+
const relToSrc = path.relative(src, target);
72+
if (!relToSrc.startsWith("..") && !path.isAbsolute(relToSrc)) {
73+
target = path.join(dest, relToSrc);
74+
}
75+
}
76+
try {
77+
await fs.promises.symlink(target, destLink);
78+
} catch {}
79+
copied++;
80+
reportProgress();
81+
}
82+
}
83+
84+
module.exports = { collectFiles, copyDirWithProgress };

0 commit comments

Comments
 (0)