diff --git a/packages/cli/package.json b/packages/cli/package.json index af151f8..e07d4c9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mosaic/cli", - "version": "0.0.4", + "version": "0.0.5", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills b/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills index 45ea77d..f044988 100755 --- a/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills +++ b/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills @@ -56,25 +56,39 @@ if [[ $fetch -eq 1 ]]; then if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR" - # Stash any local changes (dirty index or worktree) before pulling - local_changes=0 - if ! git -C "$SKILLS_REPO_DIR" diff --quiet 2>/dev/null || \ - ! git -C "$SKILLS_REPO_DIR" diff --cached --quiet 2>/dev/null; then - local_changes=1 - echo "[mosaic-skills] Stashing local changes..." - git -C "$SKILLS_REPO_DIR" stash push -q -m "mosaic-sync-skills auto-stash" + # Detect ANY dirty state: modified, staged, or untracked files. + # Use git status --porcelain which catches everything pull --rebase cares about. + dirty="" + dirty="$(git -C "$SKILLS_REPO_DIR" status --porcelain 2>/dev/null || true)" + + if [[ -n "$dirty" ]]; then + echo "[mosaic-skills] Stashing local changes (including untracked)..." + git -C "$SKILLS_REPO_DIR" stash push -q --include-untracked -m "mosaic-sync-skills auto-stash" 2>/dev/null || { + echo "[mosaic-skills] WARN: stash failed — skipping pull, using existing checkout" >&2 + dirty="skip-pull" + } fi - if ! git -C "$SKILLS_REPO_DIR" pull --rebase; then - echo "[mosaic-skills] WARN: pull failed — continuing with existing checkout" >&2 + if [[ "$dirty" != "skip-pull" ]]; then + if ! git -C "$SKILLS_REPO_DIR" pull --rebase 2>/dev/null; then + echo "[mosaic-skills] WARN: pull failed — continuing with existing checkout" >&2 + # Abort any in-progress rebase so the repo isn't left in a broken state + git -C "$SKILLS_REPO_DIR" rebase --abort 2>/dev/null || true + fi fi - # Restore stashed changes - if [[ $local_changes -eq 1 ]]; then + # Restore stashed changes if we stashed anything + if [[ -n "$dirty" && "$dirty" != "skip-pull" ]]; then echo "[mosaic-skills] Restoring local changes..." git -C "$SKILLS_REPO_DIR" stash pop -q 2>/dev/null || \ echo "[mosaic-skills] WARN: stash pop had conflicts — check $SKILLS_REPO_DIR" >&2 fi + + # Hint: customized skills belong in skills-local/, not in the canonical repo + if [[ -n "$dirty" ]]; then + echo "[mosaic-skills] TIP: Put customized skills in $MOSAIC_LOCAL_SKILLS_DIR/ instead" + echo "[mosaic-skills] Files there are never overwritten and take precedence." + fi else echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR" mkdir -p "$(dirname "$SKILLS_REPO_DIR")" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 2e4d272..5be115f 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -1,6 +1,6 @@ { "name": "@mosaic/mosaic", - "version": "0.0.4", + "version": "0.0.5", "description": "Mosaic agent framework — installation wizard and meta package", "type": "module", "main": "dist/index.js", diff --git a/packages/mosaic/src/runtime/update-checker.ts b/packages/mosaic/src/runtime/update-checker.ts index 0d9878e..cc67cf6 100644 --- a/packages/mosaic/src/runtime/update-checker.ts +++ b/packages/mosaic/src/runtime/update-checker.ts @@ -122,10 +122,18 @@ export function semverLt(a: string, b: string): boolean { // ─── Cache ────────────────────────────────────────────────────────────────── -function readCache(): UpdateCheckResult | null { +/** Cache stores only the latest registry version (the expensive network call). + * The installed version is always checked fresh — it's a local `npm ls`. */ +interface RegistryCache { + latest: string; + checkedAt: string; + registry: string; +} + +function readCache(): RegistryCache | null { try { if (!existsSync(CACHE_FILE)) return null; - const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as UpdateCheckResult; + const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as RegistryCache; const age = Date.now() - new Date(raw.checkedAt).getTime(); if (age > CACHE_TTL_MS) return null; return raw; @@ -134,10 +142,10 @@ function readCache(): UpdateCheckResult | null { } } -function writeCache(result: UpdateCheckResult): void { +function writeCache(entry: RegistryCache): void { try { mkdirSync(CACHE_DIR, { recursive: true }); - writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2) + '\n', 'utf-8'); + writeFileSync(CACHE_FILE, JSON.stringify(entry, null, 2) + '\n', 'utf-8'); } catch { // Best-effort — cache is not critical } @@ -174,29 +182,40 @@ export function getLatestVersion(): string { } /** - * Perform an update check — uses cache when fresh, otherwise hits registry. + * Perform an update check — uses registry cache when fresh, always checks + * installed version fresh (local npm ls is cheap, caching it causes stale + * "update available" banners after an upgrade). * Never throws. */ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult { + const current = getInstalledVersion(); + + let latest: string; + let checkedAt: string; + if (!options?.skipCache) { const cached = readCache(); - if (cached) return cached; + if (cached) { + latest = cached.latest; + checkedAt = cached.checkedAt; + } else { + latest = getLatestVersion(); + checkedAt = new Date().toISOString(); + writeCache({ latest, checkedAt, registry: REGISTRY }); + } + } else { + latest = getLatestVersion(); + checkedAt = new Date().toISOString(); + writeCache({ latest, checkedAt, registry: REGISTRY }); } - const current = getInstalledVersion(); - const latest = getLatestVersion(); - const updateAvailable = !!(current && latest && semverLt(current, latest)); - - const result: UpdateCheckResult = { + return { current, latest, - updateAvailable, - checkedAt: new Date().toISOString(), + updateAvailable: !!(current && latest && semverLt(current, latest)), + checkedAt, registry: REGISTRY, }; - - writeCache(result); - return result; } /**