Compare commits

..

7 Commits

Author SHA1 Message Date
Ethanfel 4ffaddef7c docs: document 7-day trial-install for missing nodes; bump to 1.7.0
Publish to Comfy registry / Publish Custom Node to registry (push) Has been cancelled
2026-06-22 17:57:46 +02:00
Ethanfel 19b96de322 feat(install): wire Install 7d / Install buttons 2026-06-22 17:56:41 +02:00
Ethanfel 406a4bf9ff feat(install): Missing rows offer Install 7d + Install 2026-06-22 17:48:37 +02:00
Ethanfel 0707a76768 feat(install): handleTrialInstall orchestrator with Manager fallback 2026-06-22 17:48:10 +02:00
Ethanfel a67b628140 feat(install): install-target resolution helpers for missing packs 2026-06-22 17:47:46 +02:00
Ethanfel 73185ad638 docs: implementation plan for 7-day trial-install of missing nodes
5-task frontend-only plan: install-resolution helpers, handleTrialInstall
orchestrator (reusing the Manager install queue + existing trial routes +
processExpiredTrials expiry), Install 7d / Install buttons + wiring, and
docs/version bump to 1.7.0. Manual verification + node --check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:37:02 +02:00
Ethanfel cc1369a38b docs: design for 7-day trial-install of missing nodes
Extend the rolling 7-day trial from disabled packs to missing (not-installed)
ones: Workflow tab Missing rows get Install 7d (real install + trial) and
Install (permanent). Expiry auto-disables (reuses processExpiredTrials, zero
new expiry code). Install spec resolved from not-installed getlist entries;
resulting dir discovered by re-reading getlist to key the trial.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:35:20 +02:00
5 changed files with 496 additions and 14 deletions
+13 -6
View File
@@ -16,7 +16,7 @@ A ComfyUI custom node package that silently tracks which nodes, packages, and mo
- **Uninstall detection** — removed packages/models are flagged separately, historical data preserved
- **Expandable detail** — click any package to see individual node-level stats
- **One-click disable** — disable unused packages straight from the dialog via ComfyUI Manager (per-package or in bulk), reversible at any time
- **Workflow tab** — on loading a workflow, splits unresolved nodes into *Missing* (install via Manager) and *Disabled*, with a temporary **Enable 7d** trial that auto-disables packages left unused
- **Workflow tab** — on loading a workflow, splits unresolved nodes into *Missing* (install permanently or on a trial) and *Disabled* (enable permanently or on a trial), with a rolling **7-day trial** that auto-disables packages left unused
- **Mirror search** — a standalone palette (⌕ button / `Ctrl/Cmd+Shift+D`) that searches nodes belonging to currently-disabled packages, draws an imitation node box (real inputs/widgets/outputs, parsed from source), and re-enables the pack on the spot
- **Non-blocking** — DB writes happen in a background thread, no impact on workflow execution
@@ -87,15 +87,22 @@ Whenever you load a workflow, the extension scans for node types the running
ComfyUI can't resolve and, if any are found, opens the dialog on the **Workflow**
tab. Unresolved nodes are split into two groups:
- **Missing** — the owning package isn't installed. Install is handled by
[ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) like always: the
**Install** button opens Manager's Custom Nodes Manager (use its *Missing*
filter).
- **Missing** — the owning package isn't installed. Each row offers:
- **Install 7d** — really install the package (via
[ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager)) and start a
*temporary trial*, so trying out someone else's workflow stays
non-committal — anything you don't actually use auto-disables.
- **Install** — install permanently (no trial).
Both take effect after a ComfyUI restart. If the install can't be resolved or
Manager refuses it (e.g. a blocked git URL), the buttons fall back to opening
Manager's Custom Nodes Manager (use its *Missing* filter).
- **Disabled** — the package is installed but currently disabled. Each row offers:
- **Enable 7d** — re-enable the package and start a *temporary trial*.
- **Enable** — re-enable permanently (no trial).
**The temporary trial** is a rolling budget of **7 distinct boot-days**. A
**The temporary trial** (started by either *Install 7d* or *Enable 7d*) is a
rolling budget of **7 distinct boot-days**. A
"boot-day" is counted at most once per calendar day, the first time ComfyUI
starts that day — so the clock measures days you actually run ComfyUI, not wall
time. **Any execution that uses the package resets the counter to zero.** If a
@@ -0,0 +1,133 @@
# Design: Trial-install (7-day) for missing nodes on workflow load
Date: 2026-06-22
Status: Approved
## Summary
Extend the existing 7-day rolling trial from *disabled* packages to *missing*
(not-installed) ones. When a loaded workflow references node types whose owning
pack isn't installed, the Workflow tab's **Missing** section gains:
- **Install 7d** — really install the pack via ComfyUI Manager, then register a
7-day trial. If it isn't used (executed) within 7 distinct boot-days, it
auto-disables (moves to `.disabled`, reversible) via the existing expiry path.
- **Install** — really install the pack permanently (no trial).
This makes trying out someone else's workflow non-committal: pull in what it
needs, and anything you don't actually use gets parked in `.disabled` instead of
permanently bloating the install (which slows boot — the whole point of this
extension).
## Decisions (from clarification)
- **Expiry action:** auto-**disable** (not uninstall). This is free: the trial
table is keyed by package and `processExpiredTrials()` already disables any
expired trial pack and clears its row. No new expiry code.
- **Missing-row actions:** both **Install 7d** and **Install** (permanent),
symmetric with the Disabled section's Enable 7d / Enable. Both perform real
installs from our UI; the current "open ComfyUI Manager" behavior becomes the
failure fallback.
## Current state it builds on (v1.6.0, verified)
- `tracker.py`: `trial_packages` table + `start_trial` / `tick_boot_days` /
`reset_trials_for` / `stop_trial` / `get_trials`. Boot tick + usage reset +
routes (`/nodes-stats/trials[/start|/stop]`) wired in `__init__.py`.
- `js/nodes_stats.js`:
- `classifyUnresolved()` (≈624) → `{ disabled[], missing[] }`; missing entries
are `{ type, pkg: <getmappings packKey> }` where packKey is a dir name,
registry id, or repo/gist URL.
- `handleInstall(pkg, dialog)` (≈1155) currently only opens Manager's Custom
Nodes Manager.
- `runManagerEnable(payload)` (≈701) = reset → POST `/manager/queue/install`
→ start → `waitForQueue` (install and enable share this endpoint; only the
payload differs).
- `processExpiredTrials()` (≈1192) disables expired trial packs via Manager,
then `stopTrial`.
- `fetchManagerInfo()` (≈483) returns installed/disabled packs only — it
**skips** `state==="not-installed"`, so registry data for missing packs is
not in hand and must be fetched separately.
## Install resolution (the only real new logic)
Not-installed `getlist` entries expose enough to install (verified live):
- CNR pack: `id` = cnr_id, `version` = semver, `files` = `[git url]`.
- Git-only pack: no `id`, `version` = `"unknown"`, `files` = `[git url]`.
Plan:
1. **`resolveInstallTarget(packKey)`** — fetch
`/customnode/getlist?mode=local&skip_update=true`, index `not-installed`
entries by every identifier (key, `id`, `files`, `repository`) with the same
normalize used in `classifyUnresolved` (lowercase, strip trailing `/`,
`.git`), and return the matching entry (or null).
2. **`installPayload(entry)`** — mirror Manager's installNodes:
`{ id: entry.id || <key>, version: entry.version, files: entry.files,
channel:"default", mode:"cache", selected_version: entry.version==="unknown"
? "unknown" : "latest", skip_post_install:false, ui_id:<key> }`.
3. Install via the existing `runManagerEnable(payload)` (same queue endpoint).
4. **`findInstalledDir(entry)`** — re-fetch getlist; find the now-installed entry
matching `entry` by id/files/repo; its **key is the directory name** (needed
to key the trial). Fallback: repo basename of `files[0]` (strip `.git`).
5. For Install 7d: `POST /nodes-stats/trials/start { package: <dir name> }`.
6. Show the restart banner: "Installed X — restart to load it" (+ " for a 7-day
trial").
## Components (all in `js/nodes_stats.js`)
- `resolveInstallTarget(packKey)`, `installPayload(entry)`, `findInstalledDir(entry)`
— small, mostly-pure helpers.
- `handleTrialInstall(pkg, dialog, temporary)` — orchestrates resolve → install →
dir discovery → (trial start if temporary) → restart banner; on any failure
falls back to the existing open-Manager behavior of `handleInstall`.
- Missing rows render `[Install 7d]` + `[Install]`; wire them in
`wireWorkflowButtons`. Reuse `setWorkflowButtonsBusy`, `notify`,
`showRestartBanner`, `managerIsBusy`.
## Data flow
```
Workflow tab (missing row)
Install 7d -> resolveInstallTarget(packKey) -> installPayload
-> runManagerEnable (POST install, start, wait)
-> findInstalledDir -> trials/start{dir}
-> restart banner "installed for 7-day trial"
Install -> same, minus trials/start
later boots -> tracker.tick_boot_days (distinct days)
use in prompt-> tracker.reset_trials_for (counter -> 0)
expiry -> processExpiredTrials (UI load) -> Manager disable + stopTrial
```
## Error handling
- ComfyUI Manager absent → Missing actions inert (as today).
- `resolveInstallTarget` miss, install HTTP error, or git-url install blocked by
Manager security level → toast + fall back to `handleInstall`'s open-Manager
guidance. No crash.
- `findInstalledDir` returns nothing (install didn't land) → don't register a
trial; toast "installed; couldn't register trial — enable/disable manually".
- `managerIsBusy()` → ask the user to retry (same guard as enable/disable).
## Testing
- Backend unchanged → no new pytest; existing `tests/` stays green.
- Pure helpers (`resolveInstallTarget` matching, `installPayload`,
`findInstalledDir` matching) written standalone; `node --check` for syntax.
- Manual: load a workflow needing a not-installed CNR pack → Install 7d installs
it, `/nodes-stats/trials` shows the resulting dir, restart banner appears;
Install (permanent) installs without a trial row; a git-url-only/blocked pack
falls back to opening Manager; Manager-absent path inert.
## Files touched
- `js/nodes_stats.js` — resolution helpers, `handleTrialInstall`, Missing-row
buttons + wiring.
- `README.md`, `pyproject.toml` — docs + version bump (1.6.0 → 1.7.0).
## Out of scope (YAGNI)
- Auto-uninstall on expiry (chose disable; reversible + free).
- Bulk "install all missing 7d" (start per-row; revisit if wanted).
- Replicating Manager's full dependency/security UX (fall back to Manager).
+246
View File
@@ -0,0 +1,246 @@
# 7-Day Trial-Install for Missing Nodes — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Let the Workflow tab really install a workflow's missing (not-installed) packs — either permanently or on the existing 7-day rolling trial that auto-disables unused packs.
**Architecture:** Frontend-only (`js/nodes_stats.js`). Resolve a missing pack's install spec from its not-installed `getlist` entry, install via ComfyUI Manager's queue (reusing `runManagerEnable`, which posts to `/manager/queue/install`), discover the resulting directory name by re-reading `getlist`, and register a trial via the existing `/nodes-stats/trials/start`. Expiry (auto-disable) is already handled by `processExpiredTrials()` — no new expiry code. On any failure, fall back to the current open-Manager behavior.
**Tech Stack:** Vanilla JS ComfyUI extension; ComfyUI Manager HTTP endpoints; the trial machinery already shipped in v1.6.0.
**Design doc:** `docs/plans/2026-06-22-trial-install-design.md`
**Testing note:** No JS test harness. After every edit run:
`cp js/nodes_stats.js /tmp/c.mjs && node --check /tmp/c.mjs && echo OK && rm /tmp/c.mjs`
Behavior is verified manually in ComfyUI (hard-refresh after each change).
**Confirmed reuse points (v1.6.0, real line numbers):** `classifyUnresolved` (≈624, builds `missing[]`), `buildWorkflowTabContent` Missing section (≈410-422), `wireWorkflowButtons` (≈698), `handleInstall` (≈1155, open-Manager fallback), `runManagerEnable` (≈701, install-queue helper), `processExpiredTrials` (≈1192), `setWorkflowButtonsBusy` (≈1172), `managerIsBusy`, `showRestartBanner`, `notify`, `escapeHtml`, `escapeAttr`.
---
### Task 1: Shared `normKey` + install-resolution helpers
**Files:** Modify `js/nodes_stats.js` (add helpers near `classifyUnresolved`, ~line 620).
**Step 1: Add a module-level `normKey`** (and point `classifyUnresolved`'s local `norm` at it for DRY):
```js
// Normalize an identifier (dir name, registry id, or repo URL) for matching.
function normKey(s) {
return String(s).trim().replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
}
```
In `classifyUnresolved`, replace the local
`const norm = (s) => String(s).trim().replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();`
with `const norm = normKey;`.
**Step 2: Add the resolver, payload builder, and dir finder:**
```js
// Find the not-installed getlist entry for a missing pack key (from getmappings).
// Returns { key, id?, version, files, repository, ... } or null.
async function resolveInstallTarget(packKey) {
let packs;
try {
const r = await fetch("/customnode/getlist?mode=local&skip_update=true");
if (!r.ok) return null;
packs = (await r.json()).node_packs;
} catch { return null; }
if (!packs) return null;
const want = normKey(packKey);
for (const [key, v] of Object.entries(packs)) {
if (!v || v.state !== "not-installed") continue;
const ids = [key, v.id, v.repository, ...(v.files || [])].filter(Boolean).map(normKey);
if (ids.includes(want)) return { key, ...v };
}
return null;
}
// Build the /manager/queue/install payload, mirroring Manager's installNodes.
function installPayload(entry, packKey) {
const unknown = !entry.version || entry.version === "unknown";
const id = entry.id || packKey;
return {
id, version: entry.version || "unknown", files: entry.files,
channel: "default", mode: "cache",
selected_version: unknown ? "unknown" : "latest",
skip_post_install: false, ui_id: id,
};
}
// After install, find the now-installed directory name (the trial key) by
// re-reading getlist and matching the entry; fall back to the repo basename.
async function findInstalledDir(entry) {
let packs = null;
try {
const r = await fetch("/customnode/getlist?mode=local&skip_update=true");
if (r.ok) packs = (await r.json()).node_packs;
} catch { /* fall through to basename */ }
if (packs) {
const want = [entry.id, entry.repository, ...(entry.files || [])].filter(Boolean).map(normKey);
for (const [key, v] of Object.entries(packs)) {
if (!v || v.state === "not-installed") continue;
const cand = [key, v.id, v.repository, ...(v.files || [])].filter(Boolean).map(normKey);
if (cand.some((c) => want.includes(c))) return key; // key = directory name
}
}
const url = (entry.files && entry.files[0]) || entry.repository || "";
return url.replace(/\/+$/, "").replace(/\.git$/i, "").split("/").pop() || null;
}
```
**Step 3: Verify syntax** — run the `node --check` line. Expected `OK`.
**Step 4: Verify resolver (browser console, after hard-refresh)** — pick a known not-installed pack key from `await (await fetch('/customnode/getlist?mode=local&skip_update=true')).json()` and confirm `resolveInstallTarget(key)` returns it. Expected: the entry with `files`/`version`.
**Step 5: Commit**
```bash
git add js/nodes_stats.js
git commit -m "feat(install): install-target resolution helpers for missing packs"
```
---
### Task 2: `handleTrialInstall` orchestrator
**Files:** Modify `js/nodes_stats.js` (add next to `handleInstall`, ~line 1155).
**Step 1: Add the orchestrator** (keep `handleInstall` as the fallback):
```js
// Real install of a missing pack, optionally on a 7-day trial. Resolves the
// install spec, installs via Manager, discovers the resulting directory, and
// (for a trial) registers it. Falls back to opening Manager on any failure.
async function handleTrialInstall(pkg, dialog, temporary) {
if (await managerIsBusy()) {
notify("ComfyUI Manager is busy. Please try again in a moment.", "warn");
return;
}
const target = await resolveInstallTarget(pkg);
if (!target) { await handleInstall(pkg, dialog); return; }
setWorkflowButtonsBusy(dialog, true);
try {
await runManagerEnable(installPayload(target, pkg)); // shared /manager/queue/install flow
const dir = temporary ? await findInstalledDir(target) : null;
if (temporary && dir) {
await fetch("/nodes-stats/trials/start", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ package: dir }),
});
}
showRestartBanner(dialog);
if (temporary && !dir) {
notify(`Installed ${pkg}, but couldn't register the trial — manage it manually. Restart to load.`, "warn");
} else {
notify(`Installed ${pkg}${temporary ? " for a 7-day trial" : ""}. Restart ComfyUI to load it.`, "success");
}
} catch (e) {
notify("Install failed: " + e.message + " — opening ComfyUI Manager.", "warn");
await handleInstall(pkg, dialog);
} finally {
setWorkflowButtonsBusy(dialog, false);
}
}
```
**Step 2: Verify syntax**`node --check`. Expected `OK`.
**Step 3: Commit**
```bash
git add js/nodes_stats.js
git commit -m "feat(install): handleTrialInstall orchestrator with Manager fallback"
```
---
### Task 3: Missing rows — `Install 7d` + `Install` buttons
**Files:** Modify `js/nodes_stats.js``buildWorkflowTabContent` Missing section (~410-422).
**Step 1: Update the subtitle and the action cell.** Replace the section header subtitle "Not installed — install via ComfyUI Manager" with "Not installed — install, optionally on a 7-day trial", and replace the single-button cell:
```js
<td style="padding:6px 8px;text-align:right;white-space:nowrap;">
${m.pkg
? `<button class="ns-btn ns-install-temp-btn" data-pkg="${escapeAttr(m.pkg)}">Install 7d</button>
<button class="ns-btn ns-install-perm-btn" data-pkg="${escapeAttr(m.pkg)}" style="margin-left:6px;">Install</button>`
: "&mdash;"}
</td>
```
**Step 2: Verify syntax**`node --check`. Expected `OK`.
**Step 3: Commit**
```bash
git add js/nodes_stats.js
git commit -m "feat(install): Missing rows offer Install 7d + Install"
```
---
### Task 4: Wire the new buttons + busy state
**Files:** Modify `js/nodes_stats.js``wireWorkflowButtons` (~698) and `setWorkflowButtonsBusy` (~1172).
**Step 1:** In `wireWorkflowButtons`, replace the `.ns-install-btn` wiring with:
```js
dialog.querySelectorAll(".ns-install-temp-btn").forEach((b) =>
b.addEventListener("click", (e) => { e.stopPropagation(); handleTrialInstall(b.dataset.pkg, dialog, true); }));
dialog.querySelectorAll(".ns-install-perm-btn").forEach((b) =>
b.addEventListener("click", (e) => { e.stopPropagation(); handleTrialInstall(b.dataset.pkg, dialog, false); }));
```
**Step 2:** In `setWorkflowButtonsBusy`, update the selector to the new classes:
```js
dialog.querySelectorAll(".ns-enable-temp-btn, .ns-enable-perm-btn, .ns-install-temp-btn, .ns-install-perm-btn").forEach((b) => {
b.disabled = busy;
});
```
**Step 3: Verify syntax**`node --check`. Expected `OK`.
**Step 4: Verify (manual)** — hard-refresh; load a workflow that needs a not-installed **CNR** pack (small one, e.g. a simple utility pack). Click **Install 7d** → it installs, restart banner appears, and `await (await fetch('/nodes-stats/trials')).json()` lists the installed dir. Click **Install** on another → installs, no trial row. Try a pack that fails / git-url-blocked → falls back to opening Manager with a toast.
**Step 5: Commit**
```bash
git add js/nodes_stats.js
git commit -m "feat(install): wire Install 7d / Install buttons"
```
---
### Task 5: Docs + version bump
**Files:** Modify `README.md`, `pyproject.toml`.
**Step 1:** In the README's Workflow-tab section, document that Missing nodes can now be installed permanently or on a 7-day trial (auto-disables if unused, takes effect after restart, falls back to ComfyUI Manager on failure). Update any feature bullet.
**Step 2:** Bump `version` in `pyproject.toml` from `1.6.0` to `1.7.0`.
**Step 3: Verify**`python -m pytest -q` (unchanged, green) and the `node --check` line (`OK`).
**Step 4: Commit**
```bash
git add README.md pyproject.toml
git commit -m "docs: document 7-day trial-install for missing nodes; bump to 1.7.0"
```
---
## Done criteria
- Workflow tab Missing rows show **Install 7d** and **Install**.
- Install 7d really installs the pack (CNR via registry; git-url where Manager allows) and registers a trial keyed by the installed directory; Install does the same without a trial.
- Unused trial-installed packs auto-disable on a later UI load via the existing `processExpiredTrials` (no new expiry code); using one resets its 7-day counter.
- Failures (unresolvable spec, HTTP error, git-url blocked, Manager absent) fall back to opening ComfyUI Manager — no crash.
- `python -m pytest -q` green; `node --check` clean.
```
+103 -7
View File
@@ -408,14 +408,17 @@ function buildWorkflowTabContent({ disabled, missing }, trials) {
html += `</tbody></table>`;
}
if (missing.length) {
html += sectionHeader("Missing", "Not installed — install via ComfyUI Manager", "#e44");
html += sectionHeader("Missing", "Not installed — install, optionally on a 7-day trial", "#e44");
html += `<table style="width:100%;border-collapse:collapse;margin-bottom:12px;"><tbody>`;
for (const m of missing) {
html += `<tr class="ns-row-safe_to_remove" style="border-bottom:1px solid #222;">
<td style="padding:6px 8px;color:#fff;">${escapeHtml(m.type)}</td>
<td style="padding:6px 8px;color:#888;">${m.pkg ? escapeHtml(m.pkg) : "unknown"}</td>
<td style="padding:6px 8px;text-align:right;">
${m.pkg ? `<button class="ns-btn ns-install-btn" data-pkg="${escapeAttr(m.pkg)}">Install</button>` : "&mdash;"}
<td style="padding:6px 8px;text-align:right;white-space:nowrap;">
${m.pkg
? `<button class="ns-btn ns-install-temp-btn" data-pkg="${escapeAttr(m.pkg)}">Install 7d</button>
<button class="ns-btn ns-install-perm-btn" data-pkg="${escapeAttr(m.pkg)}" style="margin-left:6px;">Install</button>`
: "&mdash;"}
</td></tr>`;
}
html += `</tbody></table>`;
@@ -621,6 +624,11 @@ function filterCatalog(catalog, query, limit = 50) {
// Split unresolved node types into packages that are installed-but-disabled
// (re-enable to use) vs not installed (install via Manager). Reconciles
// ComfyUI Manager's getmappings (class_type -> pack key) against getlist state.
// Normalize an identifier (dir name, registry id, or repo URL) for matching.
function normKey(s) {
return String(s).trim().replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
}
async function classifyUnresolved(types) {
if (!types.length) return { disabled: [], missing: [] };
let mappings = {}, managerInfo = null;
@@ -643,7 +651,7 @@ async function classifyUnresolved(types) {
// Index installed/disabled packs by every identifier they expose (dir name,
// id, cnr_id, aux_id, and each repo URL) so a getmappings key in any of those
// forms resolves. URLs are normalized (drop trailing slash / .git, lowercase).
const norm = (s) => String(s).trim().replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
const norm = normKey;
const byAnyKey = {};
if (managerInfo) for (const [dir, info] of Object.entries(managerInfo)) {
const rec = { ...info, _dir: dir };
@@ -662,6 +670,57 @@ async function classifyUnresolved(types) {
return { disabled, missing };
}
// Find the not-installed getlist entry for a missing pack key (from getmappings).
// Returns { key, id?, version, files, repository, ... } or null.
async function resolveInstallTarget(packKey) {
let packs;
try {
const r = await fetch("/customnode/getlist?mode=local&skip_update=true");
if (!r.ok) return null;
packs = (await r.json()).node_packs;
} catch { return null; }
if (!packs) return null;
const want = normKey(packKey);
for (const [key, v] of Object.entries(packs)) {
if (!v || v.state !== "not-installed") continue;
const ids = [key, v.id, v.repository, ...(v.files || [])].filter(Boolean).map(normKey);
if (ids.includes(want)) return { key, ...v };
}
return null;
}
// Build the /manager/queue/install payload, mirroring Manager's installNodes.
function installPayload(entry, packKey) {
const unknown = !entry.version || entry.version === "unknown";
const id = entry.id || packKey;
return {
id, version: entry.version || "unknown", files: entry.files,
channel: "default", mode: "cache",
selected_version: unknown ? "unknown" : "latest",
skip_post_install: false, ui_id: id,
};
}
// After install, find the now-installed directory name (the trial key) by
// re-reading getlist and matching the entry; fall back to the repo basename.
async function findInstalledDir(entry) {
let packs = null;
try {
const r = await fetch("/customnode/getlist?mode=local&skip_update=true");
if (r.ok) packs = (await r.json()).node_packs;
} catch { /* fall through to basename */ }
if (packs) {
const want = [entry.id, entry.repository, ...(entry.files || [])].filter(Boolean).map(normKey);
for (const [key, v] of Object.entries(packs)) {
if (!v || v.state === "not-installed") continue;
const cand = [key, v.id, v.repository, ...(v.files || [])].filter(Boolean).map(normKey);
if (cand.some((c) => want.includes(c))) return key; // key = directory name
}
}
const url = (entry.files && entry.files[0]) || entry.repository || "";
return url.replace(/\/+$/, "").replace(/\.git$/i, "").split("/").pop() || null;
}
// Build the payload ComfyUI Manager's /manager/queue/disable expects, mirroring
// Manager's own frontend: id = directory name, version = install state
// ("nightly" / semver / "unknown"), and files (repo URL) only for "unknown".
@@ -700,8 +759,10 @@ function wireWorkflowButtons(dialog) {
b.addEventListener("click", (e) => { e.stopPropagation(); handleEnable(b.dataset.pkg, true, dialog); }));
dialog.querySelectorAll(".ns-enable-perm-btn").forEach((b) =>
b.addEventListener("click", (e) => { e.stopPropagation(); handleEnable(b.dataset.pkg, false, dialog); }));
dialog.querySelectorAll(".ns-install-btn").forEach((b) =>
b.addEventListener("click", (e) => { e.stopPropagation(); handleInstall(b.dataset.pkg, dialog); }));
dialog.querySelectorAll(".ns-install-temp-btn").forEach((b) =>
b.addEventListener("click", (e) => { e.stopPropagation(); handleTrialInstall(b.dataset.pkg, dialog, true); }));
dialog.querySelectorAll(".ns-install-perm-btn").forEach((b) =>
b.addEventListener("click", (e) => { e.stopPropagation(); handleTrialInstall(b.dataset.pkg, dialog, false); }));
}
async function handleDisable(pkgNames, dialog, managerInfo) {
@@ -1169,8 +1230,43 @@ async function handleInstall(pkg, dialog) {
);
}
// Real install of a missing pack, optionally on a 7-day trial. Resolves the
// install spec, installs via Manager, discovers the resulting directory, and
// (for a trial) registers it. Falls back to opening Manager on any failure.
async function handleTrialInstall(pkg, dialog, temporary) {
if (await managerIsBusy()) {
notify("ComfyUI Manager is busy. Please try again in a moment.", "warn");
return;
}
const target = await resolveInstallTarget(pkg);
if (!target) { await handleInstall(pkg, dialog); return; }
setWorkflowButtonsBusy(dialog, true);
try {
await runManagerEnable(installPayload(target, pkg)); // shared /manager/queue/install flow
const dir = temporary ? await findInstalledDir(target) : null;
if (temporary && dir) {
await fetch("/nodes-stats/trials/start", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ package: dir }),
});
}
showRestartBanner(dialog);
if (temporary && !dir) {
notify(`Installed ${pkg}, but couldn't register the trial — manage it manually. Restart to load.`, "warn");
} else {
notify(`Installed ${pkg}${temporary ? " for a 7-day trial" : ""}. Restart ComfyUI to load it.`, "success");
}
} catch (e) {
notify("Install failed: " + e.message + " — opening ComfyUI Manager.", "warn");
await handleInstall(pkg, dialog);
} finally {
setWorkflowButtonsBusy(dialog, false);
}
}
function setWorkflowButtonsBusy(dialog, busy) {
dialog.querySelectorAll(".ns-enable-temp-btn, .ns-enable-perm-btn, .ns-install-btn").forEach((b) => {
dialog.querySelectorAll(".ns-enable-temp-btn, .ns-enable-perm-btn, .ns-install-temp-btn, .ns-install-perm-btn").forEach((b) => {
b.disabled = busy;
});
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-nodes-stats"
description = "Track usage statistics for all ComfyUI nodes and packages"
version = "1.6.0"
version = "1.7.0"
license = "MIT"
[project.urls]