Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Pre-commit hooks: formatter + linter for the TS frontend and Python backends.
# Install once with: pre-commit install
# Run on all files : pre-commit run --all-files
#
# Python (service/, home-agent/) is handled by Ruff (matches the Ruff CI check).
# WebUI is handled by ESLint + Prettier via the existing npm scripts (matches the
# "Lint TS" CI check). ESLint resolves its flat config from WebUI/, not the repo
# root, so the eslint hook runs against the whole WebUI workspace.
#
# Formatters (ruff-format, prettier) reformat in place and then `git add` what they
# changed, so they never abort the commit just because formatting was applied — the
# reformatted files are restaged and included in the same commit. The linters still
# fail on genuine, unfixable problems.
Comment on lines +10 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Sanity-check pre-commit's file-modification detection with the local ruff-format hook.
# (Run manually / locally; not runnable in this sandbox since pre-commit/uvx/ruff binaries
# and a git identity are not guaranteed to be available here.)
echo 'x=1' > /tmp/badly_formatted.py
git -C . add -f /tmp/badly_formatted.py 2>/dev/null || true
pre-commit run ruff-format --files /tmp/badly_formatted.py
echo "Exit code: $?"

Repository: TNG/AI-Playground

Length of output: 214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant config section and nearby hook definitions.
git ls-files .pre-commit-config.yaml
wc -l .pre-commit-config.yaml
sed -n '1,120p' .pre-commit-config.yaml

Repository: TNG/AI-Playground

Length of output: 2216


🌐 Web query:

pre-commit modifying files hook failed restage git add official docs formatting hook abort commit

💡 Result:

When a pre-commit hook modifies a file, the pre-commit framework intentionally fails the commit [1][2]. This design is deliberate to ensure that no changes are committed to your repository without being explicitly reviewed and staged by you [1][3]. If a hook modifies a file, it will cause the commit to abort [1][4]. To resolve this, you can follow these steps: 1. Inspect the changes made by the hook to ensure they are correct [1]. 2. Add the modified files to the staging area using git add or git add -u [1][3][4]. 3. Re-run your commit command [1][4]. Since the files are now in the index, the commit should proceed successfully [4][5]. The framework does not automatically stage these changes for you because it considers modifying the staging area an unsafe operation that could lead to unintended consequences, such as overwriting unstaged changes or committing broken code if a hook behaves unexpectedly [1][3][2]. If you find this workflow creates too much friction, you have a few options: - Manually stage the changes and re-run the commit [1][3]. - If you are confident in the hook, you can bypass the pre-commit checks using the --no-verify flag (e.g., git commit --no-verify -m "message"), though this is generally discouraged as it skips all configured hooks [4]. - If you prefer a different approach, you may consider other tools that integrate with git-commit differently, or accept that manual re-staging is the standard, safe behavior for the pre-commit framework [3][2].

Citations:


Formatter hooks still abort the first commit after rewriting files
Pre-commit still fails when ruff-format or prettier changes tracked files; git add only restages the output so the next git commit succeeds. Update the comment to reflect that this is a re-run workflow, not a one-shot commit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pre-commit-config.yaml around lines 10 - 13, The comment in the pre-commit
config still implies formatter hooks complete the commit in one pass, but the
actual flow requires rerunning the commit after `ruff-format` or `prettier`
rewrites and restages files. Update the comment near the formatter hook
configuration to describe the re-run workflow accurately, using the formatter
hook section and the git add/restage behavior as the reference point.

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
hooks:
# Linter (auto-fix what it safely can)
- id: ruff
name: ruff lint (Python)
args: [--fix]
files: ^(service|home-agent)/.*\.py$

- repo: local
hooks:
# Formatter (Ruff) — format in place, restage, never fail on reformat
- id: ruff-format
name: ruff format (Python)
language: system
entry: bash -c 'uvx ruff@0.15.20 format "$@" && git add -- "$@"' --
files: ^(service|home-agent)/.*\.py$
# Linter (ESLint with auto-fix)
- id: eslint
name: eslint (WebUI)
language: system
entry: bash -c 'cd WebUI && npm run lint:eslint'
files: ^WebUI/.*\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$
pass_filenames: false
# Formatter (Prettier) — format in place, restage, never fail on reformat
- id: prettier
name: prettier (WebUI)
language: system
entry: bash -c 'cd WebUI && files=(); for f in "$@"; do files+=("${f#WebUI/}"); done && npx prettier --write "${files[@]}" && cd .. && git add -- "$@"' --
files: ^WebUI/.*\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue|json|css|scss|html|md|ya?ml)$
97 changes: 97 additions & 0 deletions WebUI/build/scripts/ensure-precommit-hook.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
/**
* ensure-precommit-hook.mjs
*
* Install the git pre-commit hook defined by <repo-root>/.pre-commit-config.yaml
* (Ruff for the Python backends, ESLint + Prettier for the WebUI). Run as part of
* `npm run setup` so a fresh clone is ready to commit without a manual step.
*
* Best effort and idempotent:
* - No-op if this is not a git checkout (e.g. a source tarball).
* - No-op if the hook is already installed.
* - Uses `pre-commit` from PATH, else falls back to `uvx pre-commit`
* (uv ships with this repo's toolchain). If neither is available it prints
* how to install it and exits 0 — never blocks setup.
*/
import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const webuiDir = path.resolve(fileURLToPath(new URL('../../', import.meta.url)))
const repoRoot = path.resolve(webuiDir, '..')

function log(msg) {
console.log(`[ensure-precommit-hook] ${msg}`)
}

// Resolve a path relative to the git dir (handles worktrees and `.git` files).
function gitPath(relative) {
const res = spawnSync('git', ['rev-parse', '--git-path', relative], {
cwd: repoRoot,
encoding: 'utf-8',
})
if (res.status !== 0) return undefined
const out = res.stdout.trim()
return path.isAbsolute(out) ? out : path.join(repoRoot, out)
}

// Skip cleanly when this is not a git working tree.
if (
spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: repoRoot, stdio: 'ignore' })
.status !== 0
) {
log('Not a git checkout — skipping pre-commit hook install.')
process.exit(0)
}

const configPath = path.join(repoRoot, '.pre-commit-config.yaml')
if (!existsSync(configPath)) {
log(`No .pre-commit-config.yaml at ${configPath} — skipping.`)
process.exit(0)
}

// Already installed? The shim pre-commit writes carries a recognizable marker.
const hookPath = gitPath('hooks/pre-commit')
if (hookPath && existsSync(hookPath)) {
try {
if (readFileSync(hookPath, 'utf-8').includes('File generated by pre-commit')) {
log('pre-commit hook already installed — nothing to do.')
process.exit(0)
}
} catch {
/* fall through and (re)install */
}
}

function commandAvailable(cmd, args = ['--version']) {
return spawnSync(cmd, args, { stdio: 'ignore' }).status === 0
}

// Prefer a `pre-commit` on PATH; otherwise run it through uv's `uvx`.
let runner
if (commandAvailable('pre-commit')) {
runner = ['pre-commit']
} else if (commandAvailable('uvx')) {
runner = ['uvx', 'pre-commit']
} else {
log('`pre-commit` is not installed and `uvx` is unavailable, so the hook was not installed.')
log('Install it once with either of:')
log(' uv tool install pre-commit')
log(' pipx install pre-commit')
log('then re-run `npm run setup` (or `pre-commit install`).')
process.exit(0)
}

log(`Installing git pre-commit hook with: ${runner.join(' ')} install`)
const res = spawnSync(runner[0], [...runner.slice(1), 'install'], {
cwd: repoRoot,
stdio: 'inherit',
})
if (res.status !== 0) {
log('Could not install the pre-commit hook (continuing anyway).')
log('You can install it manually later with `pre-commit install`.')
process.exit(0)
}

log('pre-commit hook installed.')
36 changes: 36 additions & 0 deletions WebUI/electron/remoteUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,42 @@ export function getBundledComfyUiGitRefSync(): string {
}
}

const bundledBackendVersionCache: Partial<Record<keyof BackendVersions, BackendVersion>> = {}

/**
* Backend version/releaseTag from the **shipped** `external/backend-versions.json`
* (not a remote resolve). Used as the build-time default before the UI applies a
* resolved/overridden version via updateSettings, so the version lives in exactly
* one place (the JSON) rather than being duplicated in service source.
*/
export function getBundledBackendVersionSync(
serviceName: keyof BackendVersions,
): BackendVersion | undefined {
if (bundledBackendVersionCache[serviceName]) {
return bundledBackendVersionCache[serviceName]
}
const versionsFilePath = path.join(externalResourcesDir(), 'backend-versions.json')
try {
const raw = fs.readFileSync(versionsFilePath, 'utf-8')
const parsed = BackendVersionsSchema.parse(JSON.parse(raw))
const v = parsed[serviceName]
if (v) {
bundledBackendVersionCache[serviceName] = v
appLoggerInstance.info(
`Bundled ${serviceName} version from local JSON: ${JSON.stringify(v)}`,
'backend-version',
)
}
return v
} catch (e) {
appLoggerInstance.error(
`Failed to read bundled ${serviceName} version from ${versionsFilePath}: ${e}`,
'backend-version',
)
return undefined
}
}

const loadLocalModels = async () => {
const modelsFilePath = path.join(externalResourcesDir(), 'models.json')
try {
Expand Down
8 changes: 8 additions & 0 deletions WebUI/electron/subprocesses/comfyUIBackendService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'node:path'
import fs from 'fs'
import * as filesystem from 'fs-extra'
import { spawnProcessAsync } from './osProcessHelper.ts'
import { restoreTreeWritePermissions } from './tools.ts'
import {
LongLivedPythonApiService,
GitService,
Expand Down Expand Up @@ -816,6 +817,10 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {
// directory handles. Surface a clear, actionable error if it ultimately
// cannot be removed rather than letting `git clone` fail later with a
// confusing "destination path already exists" message.
// On Linux/macOS a read-only directory in the tree fails removal with EACCES
// (unlink/rmdir needs write permission on the parent); strip the read-only
// bit up front, and again on EACCES, so the wipe-and-reclone path works.
await restoreTreeWritePermissions(this.serviceDir)
let lastError: unknown
for (let attempt = 0; attempt < 5; attempt++) {
try {
Expand All @@ -824,6 +829,9 @@ export class ComfyUiBackendService extends LongLivedPythonApiService {
break
} catch (removeError) {
lastError = removeError
if ((removeError as NodeJS.ErrnoException)?.code === 'EACCES') {
await restoreTreeWritePermissions(this.serviceDir)
}
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
Expand Down
11 changes: 10 additions & 1 deletion WebUI/electron/subprocesses/llamaCppBackendService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from './deviceDetection.ts'
import type { LocalSettings } from '../main.ts'
import getPort, { portNumbers } from 'get-port'
import { binary, extract } from './tools.ts'
import { binary, extract, restoreTreeWritePermissions } from './tools.ts'
import * as llamaCppPhison from './llamaCppPhison.ts'
import type { LlamaCppBuildVariant } from './llamaCppPhison.ts'

Expand Down Expand Up @@ -990,6 +990,12 @@ export class LlamaCppBackendService implements ApiService {
private async removeDirectoryWithRetries(targetDir: string): Promise<void> {
const maxAttempts = 5

// On Linux/macOS, unlink()/rmdir() need write permission on the parent dir,
// so a read-only directory in an extracted release tarball would otherwise
// fail every attempt with EACCES (retries don't change permissions). Strip
// the read-only bit up front, and again before each retry, so reinstall works.
await restoreTreeWritePermissions(targetDir)

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
filesystem.removeSync(targetDir)
Expand All @@ -1004,6 +1010,9 @@ export class LlamaCppBackendService implements ApiService {
`Failed to remove ${targetDir} on attempt ${attempt}/${maxAttempts}: ${message}`,
this.name,
)
if ((error as NodeJS.ErrnoException)?.code === 'EACCES') {
await restoreTreeWritePermissions(targetDir)
}
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
Expand Down
Loading
Loading