From fa11ee8c24a26c0259008840847540c5186fc608 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 14 Aug 2026 19:12:26 +0800 Subject: [PATCH] fix(cli): honor helper refresh opt-out before locking --- .gitignore | 2 ++ .../cli/__tests__/helper-refresh.test.ts | 21 +++++++++++++++++++ .../cli/src/init/helper-refresh.ts | 6 ++++++ 3 files changed, 29 insertions(+) diff --git a/.gitignore b/.gitignore index e9bfe6f93..5166afea1 100644 --- a/.gitignore +++ b/.gitignore @@ -176,6 +176,8 @@ v3/@claude-flow/cli/plugins/ # regenerated on demand, not source. The signed manifest source of truth lives # in v3/@claude-flow/cli/.claude/helpers/helpers.manifest.json. .claude/helpers/.helpers-version +.claude/helpers/.helpers-refresh.lock +.claude/helpers/*.tmp-* .claude/helpers/helpers.manifest.json # Runtime proven-config adoption state (per-install, not source) diff --git a/v3/@claude-flow/cli/__tests__/helper-refresh.test.ts b/v3/@claude-flow/cli/__tests__/helper-refresh.test.ts index 61b3185a2..b6384ff4e 100644 --- a/v3/@claude-flow/cli/__tests__/helper-refresh.test.ts +++ b/v3/@claude-flow/cli/__tests__/helper-refresh.test.ts @@ -329,6 +329,27 @@ describe('autoRefreshHelpersIfStale', () => { } }); + it('honors .LOCKED before attempting to acquire the refresh lock', async () => { + const { cwd, helpersDir } = makeProject(); + writeFileSync(join(helpersDir, 'hook-handler.cjs'), 'HAND-MAINTAINED'); + writeFileSync(join(helpersDir, HELPERS_STAMP_FILE), '1.0.0'); + writeFileSync(join(helpersDir, '.LOCKED'), ''); + const lockPath = join(helpersDir, HELPERS_REFRESH_LOCK_FILE); + writeFileSync(lockPath, JSON.stringify({ pid: process.pid, token: 'other-owner' })); + + try { + const r = await autoRefreshHelpersIfStale(cwd, { + versionOverride: '2.0.0', + lockWaitMsOverride: 0, + }); + expect(r.refreshed).toBe(false); + expect(r.blocked).toMatch(/\.LOCKED marker present/); + expect(readFileSync(lockPath, 'utf-8')).toContain('other-owner'); + } finally { + unlinkSync(lockPath); + } + }); + it('recovers an abandoned lock created before owner metadata was written', async () => { const { cwd, helpersDir } = makeProject(); writeFileSync(join(helpersDir, 'hook-handler.cjs'), 'INITIAL-HANDLER'); diff --git a/v3/@claude-flow/cli/src/init/helper-refresh.ts b/v3/@claude-flow/cli/src/init/helper-refresh.ts index f31b35c23..633440d07 100644 --- a/v3/@claude-flow/cli/src/init/helper-refresh.ts +++ b/v3/@claude-flow/cli/src/init/helper-refresh.ts @@ -394,6 +394,12 @@ async function refreshOneHelpersDir( opts: RefreshOptions, ): Promise<{ refreshed: boolean; from?: string; to?: string; blocked?: string }> { if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs'))) return { refreshed: false }; + // Respect the repository opt-out before creating even a transient lock file + // in a directory whose helpers are intentionally maintained by hand. Keep + // the check in the locked path too in case the marker appears while waiting. + if (fs.existsSync(path.join(helpersDir, '.LOCKED'))) { + return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' }; + } try { if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version) return { refreshed: false }; } catch { /* unstamped: continue to the locked path */ }