mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* fix(cli): invalidate the skills nudge cache after a successful install/update/check The passive "N skills out of date or missing" nudge reads a 24h config cache that only the background check (on non-skills commands) ever wrote. The skills commands themselves are excluded from the nudge pipeline, so a successful `skills update`/install/check never refreshed or dropped the cached verdict — the pre-install count kept printing on every other command for up to 24h. Reconcile commands now drop the cached verdict (counts + timestamp) so the next command's background check re-runs for real. The offline presence-only path deliberately keeps the cache: that run learned nothing about freshness. * fix(skills): win32-safe npx spawns in media-use + accurate whisper wording The Whisper transcribe fallback and the Kokoro local-TTS delegation both spawned a bare "npx" via execFileSync — on Windows npx is npx.cmd, which spawn cannot exec, so both paths died with `spawnSync npx ENOENT`. Route them through the skill's existing resolveSpawnCommand (node + npx-cli.js on win32, no shell:true), same as the audio engine's TTS spawns. Also corrects the "bundled with the hyperframes CLI" claim about whisper.cpp: it is resolved from PATH / installed via Homebrew / built from source with git+cmake on first use, and models download from HuggingFace — nothing whisper is shipped in the package. * feat(skills): canonical fully-silent marker + auth status exit-code docs product-launch's Step 3.1 gate said "or the project is marked silent" but nothing defined how to mark one, and audio.mjs unconditionally retrieved BGM. Define the canonical marker — `music: none` in the storyboard's top YAML block, plus no SCRIPT.md — and honor it: audio generate produces nothing (removing stale audio_meta.json, since absence is what assemble treats as silent), and `music: none` with narration keeps TTS while turning BGM off. Also documents the `auth status` exit-code contract (exit 1 while signed out is the normal offline state, not a failure) in the product-launch Step 0 note and the CLI skill's cloud reference. * fix(skills): transient-init retry for standalone animation-map and contrast-report The standalone helpers called initializeSession exactly once, so a valid modular project — whose sub-composition timelines register asynchronously — could hit the readiness deadline and die with the transient "zero duration / Runtime ready: false" diagnostic the render pipeline retries (probeStage). Add initializeSessionWithRetry to the shared package-loader (both byte-identical copies): close the crashed session and retry once with a fresh browser, gated by the engine's canonical isTransientBrowserError — now re-exported from @hyperframes/producer, with a frozen fallback pattern list for older published packages. The "Runtime ready: true" fast-fail (a genuine authoring bug) still fails without a retry. * feat(skills): extend the fully-silent marker to faceless-explainer and pr-to-video Both workflows reuse product-launch's audio model — their Step 3.1 gates carried the same undefined "marked silent" phrase, and their (intentionally identical) audio.mjs copies had the same unconditional BGM retrieve. Port the `music: none` marker handling into both copies, define the marker in their SKILL.md Step 3.1 and story-design references, and turn the copies' "intentionally identical" header claim into a byte-identity pin test so the next fix can't silently miss one of them. * test(cli): reset the prune mock explicitly instead of relying on restoreAllMocks The converge test's toHaveBeenCalledTimes(1) held only because vitest 3's vi.restoreAllMocks() clears vi.fn() call state; vitest 4 restores spies only, so the count would accumulate across tests and fail. Reset pruneOrphanedLockEntries in beforeEach like the other manifest mocks — passes under both vitest 3.2.4 (pinned) and vitest 4. * test(skills): close review findings — package-loader pin, whisper win32 parity, quoted-none Review follow-ups on #2476: - package-loader.mjs byte-identity pin (the elevated concern): the two copies now carry initializeSessionWithRetry + FALLBACK_TRANSIENT_PATTERNS, exactly the shared-logic shape a future fix could land in one copy and miss in the other — same enforcement as the audio.mjs pin. - whisper win32 call-site parity: runWhisper's npx resolution lifted into lib/npx-sync.mjs (resolveNpxInvocation, injectable params matching the localTtsGenerate idiom) with the same three-branch coverage as the Kokoro site — plus the hard-fail contract (throws actionably, since the whisper fallback has no next provider to fall through to). - quoted music: "none" pin: the vendored storyboard parser strips matching quotes at parse time (stripQuotes), so the silent marker already accepts the quoted spelling — pinned so that stays true.
136 lines
5.6 KiB
TypeScript
136 lines
5.6 KiB
TypeScript
// Passive "your skills are stale" nudge. Mirrors updateCheck.ts: a background
|
|
// check populates a 24h cache; printSkillsUpdateNotice() reads the cache
|
|
// synchronously and prints one line on exit.
|
|
//
|
|
// Why a passive nudge (not just `skills check`): agents don't reliably run a
|
|
// check on their own, but they DO run render/lint/validate — so we piggyback
|
|
// the reminder on the commands they already run.
|
|
|
|
import { readConfig, readConfigFresh, writeConfig } from "../telemetry/config.js";
|
|
import { checkSkills } from "./skillsManifest.js";
|
|
import { updateNoticesSuppressed } from "./updateCheck.js";
|
|
|
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
export interface SkillsUpdateMeta {
|
|
updateAvailable: boolean;
|
|
outdated: number;
|
|
missing: number;
|
|
/** Installed skills flagged removed-upstream (renamed/dropped) at the last check. */
|
|
removed: number;
|
|
}
|
|
|
|
/** Synchronous read from cache — never fetches. */
|
|
function getSkillsUpdateMeta(): SkillsUpdateMeta {
|
|
const config = readConfig();
|
|
return {
|
|
updateAvailable: config.skillsUpdateAvailable ?? false,
|
|
outdated: config.skillsOutdatedCount ?? 0,
|
|
missing: config.skillsMissingCount ?? 0,
|
|
removed: config.skillsRemovedCount ?? 0,
|
|
};
|
|
}
|
|
|
|
function cacheFresh(lastSkillsCheck: string | undefined, now: number): boolean {
|
|
if (!lastSkillsCheck) return false;
|
|
return now - new Date(lastSkillsCheck).getTime() < CHECK_INTERVAL_MS;
|
|
}
|
|
|
|
/** Run the real check and persist the result to the cache. */
|
|
async function refreshSkillsCache(): Promise<SkillsUpdateMeta> {
|
|
// `canonical: true` so this nudge's counts agree with `updateSkills`'s
|
|
// source of truth — otherwise a stale in-repo skills-manifest.json (e.g.
|
|
// inside a hyperframes checkout) can produce a false-positive count here.
|
|
const result = await checkSkills({ canonical: true });
|
|
// Only record a meaningful check when skills were actually found.
|
|
if (result.location) {
|
|
const config = readConfig();
|
|
config.lastSkillsCheck = new Date().toISOString();
|
|
config.skillsUpdateAvailable = result.updateAvailable;
|
|
config.skillsOutdatedCount = result.summary.outdated;
|
|
// Core-missing only: skills that install on demand (workflows not yet
|
|
// triggered on this machine) are not "missing" worth nagging about.
|
|
config.skillsMissingCount = result.summary.coreMissing;
|
|
// Removed-upstream skills are just as reconcilable as outdated/missing
|
|
// ones (a plain `skills update` prunes them) — omitting them here is what
|
|
// made the nudge undercount (e.g. reporting "2 skills out of date or
|
|
// missing" while a 3rd, renamed/dropped skill sat unmentioned).
|
|
config.skillsRemovedCount = result.summary.removed;
|
|
writeConfig(config);
|
|
}
|
|
return {
|
|
updateAvailable: result.updateAvailable,
|
|
outdated: result.summary.outdated,
|
|
missing: result.summary.coreMissing,
|
|
removed: result.summary.removed,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Drop the cached verdict (counts + timestamp) so the next command's
|
|
* background check re-runs instead of nagging from a pre-reconcile snapshot.
|
|
*
|
|
* Called after a `skills` install/update/check has reconciled or re-measured
|
|
* the install. Those commands are excluded from the nudge pipeline entirely
|
|
* (see cli.ts), so nothing else refreshes the cache when they run — without
|
|
* this, the last background verdict (taken BEFORE the install) keeps printing
|
|
* "N skills out of date or missing" on every other command for up to 24h
|
|
* after a successful install/update.
|
|
*
|
|
* Counts are cleared along with the timestamp — not left behind — so an
|
|
* offline machine (where the next refresh fails and falls back to the cached
|
|
* meta) goes quiet rather than resurrecting the stale pre-install counts.
|
|
*
|
|
* Best-effort: a config write failure must never fail the skills command
|
|
* that just succeeded.
|
|
*/
|
|
export function invalidateSkillsCache(): void {
|
|
try {
|
|
// Fresh read narrows the lost-update window against a concurrently
|
|
// running CLI process that wrote other config fields in the meantime.
|
|
const config = readConfigFresh();
|
|
delete config.lastSkillsCheck;
|
|
delete config.skillsUpdateAvailable;
|
|
delete config.skillsOutdatedCount;
|
|
delete config.skillsMissingCount;
|
|
delete config.skillsRemovedCount;
|
|
writeConfig(config);
|
|
} catch {
|
|
// best-effort — never break the command that just reconciled skills
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh the skills freshness cache if it is older than 24h. Best-effort:
|
|
* any failure (offline, no manifest published yet, no skills installed) leaves
|
|
* the cache untouched and reports "no update".
|
|
*
|
|
* @param force - skip the cache and check now
|
|
*/
|
|
export async function checkSkillsForUpdate(force?: boolean): Promise<SkillsUpdateMeta> {
|
|
if (!force && cacheFresh(readConfig().lastSkillsCheck, Date.now())) return getSkillsUpdateMeta();
|
|
try {
|
|
return await refreshSkillsCache();
|
|
} catch {
|
|
return getSkillsUpdateMeta();
|
|
}
|
|
}
|
|
|
|
/** The stale-skills nudge text, or null when nothing is outdated, missing, or removed. */
|
|
function skillsNoticeText(meta: SkillsUpdateMeta): string | null {
|
|
const total = meta.outdated + meta.missing + meta.removed;
|
|
if (total < 1) return null;
|
|
const noun = total === 1 ? "skill" : "skills";
|
|
return `\n ${total} HyperFrames ${noun} out of date or missing.\n Run: npx hyperframes skills update\n\n`;
|
|
}
|
|
|
|
/**
|
|
* Print a one-line nudge to stderr if installed skills are stale. Same gating
|
|
* as the CLI self-update notice (CI, non-TTY, dev, HYPERFRAMES_NO_UPDATE_CHECK).
|
|
*/
|
|
export function printSkillsUpdateNotice(): void {
|
|
if (updateNoticesSuppressed()) return;
|
|
const text = skillsNoticeText(getSkillsUpdateMeta());
|
|
if (text) process.stderr.write(text);
|
|
}
|