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",
|
"name": "@mosaic/cli",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -56,25 +56,39 @@ if [[ $fetch -eq 1 ]]; then
|
|||||||
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
|
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
|
||||||
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
|
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
|
||||||
|
|
||||||
# Stash any local changes (dirty index or worktree) before pulling
|
# Detect ANY dirty state: modified, staged, or untracked files.
|
||||||
local_changes=0
|
# Use git status --porcelain which catches everything pull --rebase cares about.
|
||||||
if ! git -C "$SKILLS_REPO_DIR" diff --quiet 2>/dev/null || \
|
dirty=""
|
||||||
! git -C "$SKILLS_REPO_DIR" diff --cached --quiet 2>/dev/null; then
|
dirty="$(git -C "$SKILLS_REPO_DIR" status --porcelain 2>/dev/null || true)"
|
||||||
local_changes=1
|
|
||||||
echo "[mosaic-skills] Stashing local changes..."
|
if [[ -n "$dirty" ]]; then
|
||||||
git -C "$SKILLS_REPO_DIR" stash push -q -m "mosaic-sync-skills auto-stash"
|
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
|
fi
|
||||||
|
|
||||||
if ! git -C "$SKILLS_REPO_DIR" pull --rebase; then
|
if [[ "$dirty" != "skip-pull" ]]; then
|
||||||
echo "[mosaic-skills] WARN: pull failed — continuing with existing checkout" >&2
|
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
|
fi
|
||||||
|
|
||||||
# Restore stashed changes
|
# Restore stashed changes if we stashed anything
|
||||||
if [[ $local_changes -eq 1 ]]; then
|
if [[ -n "$dirty" && "$dirty" != "skip-pull" ]]; then
|
||||||
echo "[mosaic-skills] Restoring local changes..."
|
echo "[mosaic-skills] Restoring local changes..."
|
||||||
git -C "$SKILLS_REPO_DIR" stash pop -q 2>/dev/null || \
|
git -C "$SKILLS_REPO_DIR" stash pop -q 2>/dev/null || \
|
||||||
echo "[mosaic-skills] WARN: stash pop had conflicts — check $SKILLS_REPO_DIR" >&2
|
echo "[mosaic-skills] WARN: stash pop had conflicts — check $SKILLS_REPO_DIR" >&2
|
||||||
fi
|
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
|
else
|
||||||
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"
|
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"
|
||||||
mkdir -p "$(dirname "$SKILLS_REPO_DIR")"
|
mkdir -p "$(dirname "$SKILLS_REPO_DIR")"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/mosaic",
|
"name": "@mosaic/mosaic",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"description": "Mosaic agent framework — installation wizard and meta package",
|
"description": "Mosaic agent framework — installation wizard and meta package",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -122,10 +122,18 @@ export function semverLt(a: string, b: string): boolean {
|
|||||||
|
|
||||||
// ─── Cache ──────────────────────────────────────────────────────────────────
|
// ─── 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 {
|
try {
|
||||||
if (!existsSync(CACHE_FILE)) return null;
|
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();
|
const age = Date.now() - new Date(raw.checkedAt).getTime();
|
||||||
if (age > CACHE_TTL_MS) return null;
|
if (age > CACHE_TTL_MS) return null;
|
||||||
return raw;
|
return raw;
|
||||||
@@ -134,10 +142,10 @@ function readCache(): UpdateCheckResult | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeCache(result: UpdateCheckResult): void {
|
function writeCache(entry: RegistryCache): void {
|
||||||
try {
|
try {
|
||||||
mkdirSync(CACHE_DIR, { recursive: true });
|
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 {
|
} catch {
|
||||||
// Best-effort — cache is not critical
|
// 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.
|
* Never throws.
|
||||||
*/
|
*/
|
||||||
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
|
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
|
||||||
|
const current = getInstalledVersion();
|
||||||
|
|
||||||
|
let latest: string;
|
||||||
|
let checkedAt: string;
|
||||||
|
|
||||||
if (!options?.skipCache) {
|
if (!options?.skipCache) {
|
||||||
const cached = readCache();
|
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();
|
return {
|
||||||
const latest = getLatestVersion();
|
|
||||||
const updateAvailable = !!(current && latest && semverLt(current, latest));
|
|
||||||
|
|
||||||
const result: UpdateCheckResult = {
|
|
||||||
current,
|
current,
|
||||||
latest,
|
latest,
|
||||||
updateAvailable,
|
updateAvailable: !!(current && latest && semverLt(current, latest)),
|
||||||
checkedAt: new Date().toISOString(),
|
checkedAt,
|
||||||
registry: REGISTRY,
|
registry: REGISTRY,
|
||||||
};
|
};
|
||||||
|
|
||||||
writeCache(result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user