fix: stale update banner + skill sync dirty worktree crash (#358)
This commit was merged in pull request #358.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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")"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user