Compare commits

..

1 Commits

Author SHA1 Message Date
Jarvis
116b91d2ae fix(packages): republish @mosaic/config and bump dependents
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The published @mosaic/config@0.0.1 on the Gitea registry is the
stale tooling-configs package (tsconfig/eslint/prettier) with only
subpath exports. When the package was repurposed in 04a80fb9 as the
runtime config loader, its version was never bumped, so consumers
that pull from the registry still get the old tarball.

This caused `mosaic gateway install` to fail with
ERR_PACKAGE_PATH_NOT_EXPORTED when gateway imported loadConfig from
@mosaic/config at runtime.

- Bump @mosaic/config to 0.0.2 so CI publishes the runtime variant
- Bump @mosaic/gateway to 0.0.5 to republish with the fixed dep
  (0.1.0 was an unintended semver jump; deleted from registry to
  restore 0.0.x lineage)
- Bump @mosaic/mosaic to 0.0.19 so the CLI ships with the fixed
  transitive dep resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:51:09 -05:00
13 changed files with 38 additions and 284 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/gateway",
"version": "0.0.6",
"version": "0.0.5",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",

View File

@@ -1,34 +0,0 @@
# Scratchpad — updater package target fix (#382)
- Objective: Fix `mosaic update` so modern installs query `@mosaic/mosaic` instead of stale `@mosaic/cli`.
- Scope: updater logic, user-facing update/install hints, tests, package version bump(s).
- Constraints: preserve backward compatibility for older `@mosaic/cli` installs if practical.
- Acceptance:
- fresh installs using `@mosaic/mosaic` report latest correctly
- older installs do not regress unnecessarily
- tests cover package lookup behavior
- release version bumped for changed package(s)
## Decisions
- Prefer `@mosaic/mosaic` when both modern and legacy packages are installed globally.
- For legacy `@mosaic/cli` installs, query `@mosaic/cli` first, then fall back to `@mosaic/mosaic` if the legacy package is not published.
- Share install-target selection from `packages/mosaic` so both the consolidated CLI and the legacy `packages/cli` entrypoint print/install the same package target.
- Extend the update cache to persist the resolved target package as well as the version so cached checks preserve the migration target.
## Validation
- `pnpm install`
- `pnpm --filter @mosaic/mosaic test -- __tests__/update-checker.test.ts`
- `pnpm exec eslint --no-warn-ignored packages/mosaic/src/runtime/update-checker.ts packages/mosaic/src/cli.ts packages/mosaic/src/index.ts packages/mosaic/__tests__/update-checker.test.ts packages/cli/src/cli.ts`
- `pnpm --filter @mosaic/mosaic lint`
- pre-push hooks: `typecheck`, `lint`, `format:check`
## Review
- Manual review of the updater diff caught and fixed a cache regression where fallback results would lose the resolved package target on subsequent cached checks.
## Risks / Notes
- Direct `pnpm --filter @mosaic/mosaic typecheck` and `pnpm --filter @mosaic/cli ...` checks were not representative in this worktree because `packages/cli` is excluded from `pnpm-workspace.yaml` and the standalone package check lacked the built workspace dependency graph.
- The repo's pre-push hooks provided the authoritative validation path here and passed: root `typecheck`, `lint`, and `format:check`.

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/cli",
"version": "0.0.17",
"version": "0.0.16",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",

View File

@@ -314,8 +314,7 @@ program
.description('Check for and install Mosaic CLI updates')
.option('--check', 'Check only, do not install')
.action(async (opts: { check?: boolean }) => {
const { checkForUpdate, formatUpdateNotice, getInstallCommand } =
await import('@mosaic/mosaic');
const { checkForUpdate, formatUpdateNotice } = await import('@mosaic/mosaic');
const { execSync } = await import('node:child_process');
console.log('Checking for updates…');
@@ -345,7 +344,7 @@ program
try {
// Relies on @mosaic:registry in ~/.npmrc — do NOT pass --registry
// globally or non-@mosaic deps will 404 against the Gitea registry.
execSync(getInstallCommand(result), {
execSync('npm install -g @mosaic/cli@latest', {
stdio: 'inherit',
timeout: 60_000,
});

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/db",
"version": "0.0.3",
"version": "0.0.2",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/memory",
"version": "0.0.3",
"version": "0.0.2",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",

View File

@@ -1,40 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import { semverLt, formatUpdateNotice } from '../src/runtime/update-checker.js';
import type { UpdateCheckResult } from '../src/runtime/update-checker.js';
const { execSyncMock, cacheFiles } = vi.hoisted(() => ({
execSyncMock: vi.fn(),
cacheFiles: new Map<string, string>(),
}));
vi.mock('node:child_process', () => ({
execSync: execSyncMock,
}));
vi.mock('node:fs', () => ({
existsSync: vi.fn((path: string) => cacheFiles.has(path)),
mkdirSync: vi.fn(),
readFileSync: vi.fn((path: string) => {
const value = cacheFiles.get(path);
if (value === undefined) {
throw new Error(`ENOENT: ${path}`);
}
return value;
}),
writeFileSync: vi.fn((path: string, content: string) => {
cacheFiles.set(path, content);
}),
}));
vi.mock('node:os', () => ({
homedir: vi.fn(() => '/mock-home'),
}));
async function importUpdateChecker() {
vi.resetModules();
return import('../src/runtime/update-checker.js');
}
describe('semverLt', () => {
it('returns true when a < b', () => {
expect(semverLt('0.0.1', '0.0.2')).toBe(true);
@@ -58,11 +25,6 @@ describe('semverLt', () => {
});
describe('formatUpdateNotice', () => {
beforeEach(() => {
execSyncMock.mockReset();
cacheFiles.clear();
});
it('returns empty string when up to date', () => {
const result: UpdateCheckResult = {
current: '1.0.0',
@@ -87,109 +49,4 @@ describe('formatUpdateNotice', () => {
expect(notice).toContain('0.1.0');
expect(notice).toContain('Update available');
});
it('uses @mosaic/mosaic hints for modern installs', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies: {
'@mosaic/mosaic': { version: '0.0.17' },
'@mosaic/cli': { version: '0.0.16' },
},
});
}
if (command.includes('view @mosaic/mosaic version')) {
return '0.0.18';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
const notice = formatUpdateNotice(result);
expect(result.current).toBe('0.0.17');
expect(result.latest).toBe('0.0.18');
expect(notice).toContain('@mosaic/mosaic@latest');
expect(notice).not.toContain('@mosaic/cli@latest');
});
it('falls back to @mosaic/mosaic for legacy @mosaic/cli installs when cli is unavailable', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies: {
'@mosaic/cli': { version: '0.0.16' },
},
});
}
if (command.includes('view @mosaic/cli version')) {
throw new Error('not found');
}
if (command.includes('view @mosaic/mosaic version')) {
return '0.0.17';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
const notice = formatUpdateNotice(result);
expect(result.current).toBe('0.0.16');
expect(result.latest).toBe('0.0.17');
expect(notice).toContain('@mosaic/mosaic@latest');
});
it('does not reuse a cached modern-package result for a legacy install', async () => {
let installedPackage = '@mosaic/mosaic';
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies:
installedPackage === '@mosaic/mosaic'
? { '@mosaic/mosaic': { version: '0.0.17' } }
: { '@mosaic/cli': { version: '0.0.16' } },
});
}
if (command.includes('view @mosaic/mosaic version')) {
return installedPackage === '@mosaic/mosaic' ? '0.0.18' : '0.0.17';
}
if (command.includes('view @mosaic/cli version')) {
throw new Error('not found');
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const modernResult = checkForUpdate();
installedPackage = '@mosaic/cli';
const legacyResult = checkForUpdate();
expect(modernResult.currentPackage).toBe('@mosaic/mosaic');
expect(modernResult.targetPackage).toBe('@mosaic/mosaic');
expect(modernResult.latest).toBe('0.0.18');
expect(legacyResult.currentPackage).toBe('@mosaic/cli');
expect(legacyResult.targetPackage).toBe('@mosaic/mosaic');
expect(legacyResult.latest).toBe('0.0.17');
expect(execSyncMock).toHaveBeenCalledWith(
expect.stringContaining('view @mosaic/cli version'),
expect.any(Object),
);
expect(execSyncMock).toHaveBeenCalledWith(
expect.stringContaining('view @mosaic/mosaic version'),
expect.any(Object),
);
});
});

View File

@@ -18,7 +18,9 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./package.json": "./package.json",
"./framework/*": "./framework/*"
},
"scripts": {
"build": "tsc",

View File

@@ -12,7 +12,6 @@ import {
backgroundUpdateCheck,
checkForUpdate,
formatUpdateNotice,
getInstallCommand,
} from './runtime/update-checker.js';
import { runWizard } from './wizard.js';
import { ClackPrompter } from './prompter/clack-prompter.js';
@@ -355,7 +354,7 @@ program
try {
// Relies on @mosaic:registry in ~/.npmrc — do NOT pass --registry
// globally or non-@mosaic deps will 404 against the Gitea registry.
execSync(getInstallCommand(result), {
execSync('npm install -g @mosaic/cli@latest', {
stdio: 'inherit',
timeout: 60_000,
});

View File

@@ -7,9 +7,9 @@
import { execFileSync, execSync, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
import { createRequire } from 'node:module';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Command } from 'commander';
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
@@ -498,14 +498,10 @@ function delegateToScript(scriptPath: string, args: string[], env?: Record<strin
* CLI version) over the deployed copy in ~/.config/mosaic/ (may be stale).
*/
function resolveTool(...segments: string[]): string {
try {
const req = createRequire(import.meta.url);
const mosaicPkg = dirname(req.resolve('@mosaic/mosaic/package.json'));
const bundled = join(mosaicPkg, 'framework', 'tools', ...segments);
if (existsSync(bundled)) return bundled;
} catch {
// Fall through to deployed copy
}
// Resolve relative to the built file: dist/commands/launch.js → ../../framework/tools/...
const thisFile = fileURLToPath(import.meta.url);
const bundled = join(dirname(thisFile), '..', '..', 'framework', 'tools', ...segments);
if (existsSync(bundled)) return bundled;
return join(MOSAIC_HOME, 'tools', ...segments);
}

View File

@@ -1,11 +1 @@
export const VERSION = '0.0.0';
export {
backgroundUpdateCheck,
checkForUpdate,
formatUpdateNotice,
getInstallCommand,
getInstalledVersion,
getLatestVersion,
semverLt,
} from './runtime/update-checker.js';

View File

@@ -1,5 +1,5 @@
/**
* Mosaic update checker — compares the installed Mosaic package against the
* Mosaic update checker — compares installed @mosaic/cli version against the
* Gitea npm registry and reports when an upgrade is available.
*
* Used by:
@@ -23,12 +23,8 @@ import { join } from 'node:path';
export interface UpdateCheckResult {
/** Currently installed version (empty if not found) */
current: string;
/** Currently installed package name */
currentPackage?: string;
/** Latest published version (empty if check failed) */
latest: string;
/** Package that should be installed for the latest version */
targetPackage?: string;
/** True when a newer version is available */
updateAvailable: boolean;
/** ISO timestamp of this check */
@@ -40,9 +36,7 @@ export interface UpdateCheckResult {
// ─── Constants ──────────────────────────────────────────────────────────────
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
const MODERN_PKG = '@mosaic/mosaic';
const LEGACY_PKG = '@mosaic/cli';
const INSTALLED_PACKAGE_ORDER = [MODERN_PKG, LEGACY_PKG] as const;
const CLI_PKG = '@mosaic/cli';
const CACHE_DIR = join(homedir(), '.cache', 'mosaic');
const CACHE_FILE = join(CACHE_DIR, 'update-check.json');
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -131,20 +125,17 @@ export function semverLt(a: string, b: string): boolean {
/** 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 {
currentPackage?: string;
latest: string;
targetPackage?: string;
checkedAt: string;
registry: string;
}
function readCache(currentPackage: string): RegistryCache | null {
function readCache(): RegistryCache | null {
try {
if (!existsSync(CACHE_FILE)) return null;
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;
if ((raw.currentPackage || '') !== currentPackage) return null;
return raw;
} catch {
return null;
@@ -163,10 +154,10 @@ function writeCache(entry: RegistryCache): void {
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Get the currently installed Mosaic package version.
* Prefers the consolidated @mosaic/mosaic package over legacy @mosaic/cli.
* Get the currently installed version of @mosaic/cli.
* Returns empty string if not installed.
*/
export function getInstalledVersion(): { name: string; version: string } {
export function getInstalledVersion(): string {
// Fast path: check via package.json require chain
try {
const raw = npmExec(`ls -g --depth=0 --json 2>/dev/null`, 3000);
@@ -174,41 +165,20 @@ export function getInstalledVersion(): { name: string; version: string } {
const data = JSON.parse(raw) as {
dependencies?: Record<string, { version?: string }>;
};
for (const pkg of INSTALLED_PACKAGE_ORDER) {
const version = data?.dependencies?.[pkg]?.version;
if (version) {
return { name: pkg, version };
}
}
return data?.dependencies?.[CLI_PKG]?.version ?? '';
}
} catch {
// fall through
}
return { name: '', version: '' };
return '';
}
/**
* Fetch the latest published version from the Gitea npm registry.
* For legacy @mosaic/cli installs, try the legacy package first and fall back
* to @mosaic/mosaic to support the CLI -> mosaic package consolidation.
* Returns empty string on failure.
*/
export function getLatestVersion(installedPackage = ''): { name: string; version: string } {
const candidates =
installedPackage === LEGACY_PKG ? [LEGACY_PKG, MODERN_PKG] : [MODERN_PKG, LEGACY_PKG];
for (const pkg of candidates) {
const version = npmExec(`view ${pkg} version --registry=${REGISTRY}`);
if (version) {
return { name: pkg, version };
}
}
return { name: '', version: '' };
}
export function getInstallCommand(result: Pick<UpdateCheckResult, 'targetPackage'>): string {
return `npm i -g ${result.targetPackage || MODERN_PKG}@latest`;
export function getLatestVersion(): string {
return npmExec(`view ${CLI_PKG} version --registry=${REGISTRY}`);
}
/**
@@ -218,49 +188,31 @@ export function getInstallCommand(result: Pick<UpdateCheckResult, 'targetPackage
* Never throws.
*/
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
const currentInfo = getInstalledVersion();
const current = currentInfo.version;
const current = getInstalledVersion();
let latestInfo: { name: string; version: string };
let latest: string;
let checkedAt: string;
if (!options?.skipCache) {
const cached = readCache(currentInfo.name);
const cached = readCache();
if (cached) {
latestInfo = {
name: cached.targetPackage || MODERN_PKG,
version: cached.latest,
};
latest = cached.latest;
checkedAt = cached.checkedAt;
} else {
latestInfo = getLatestVersion(currentInfo.name);
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name,
checkedAt,
registry: REGISTRY,
});
writeCache({ latest, checkedAt, registry: REGISTRY });
}
} else {
latestInfo = getLatestVersion(currentInfo.name);
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name,
checkedAt,
registry: REGISTRY,
});
writeCache({ latest, checkedAt, registry: REGISTRY });
}
return {
current,
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name || MODERN_PKG,
updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)),
latest,
updateAvailable: !!(current && latest && semverLt(current, latest)),
checkedAt,
registry: REGISTRY,
};
@@ -272,21 +224,14 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
export function formatUpdateNotice(result: UpdateCheckResult): string {
if (!result.updateAvailable) return '';
const installCommand = getInstallCommand(result);
const targetChanged =
result.currentPackage && result.targetPackage && result.currentPackage !== result.targetPackage;
const lines = [
'',
'╭─────────────────────────────────────────────────╮',
'│ │',
`│ Update available: ${result.current}${result.latest}`.padEnd(50) + '│',
...(targetChanged
? [`│ Package target: ${result.currentPackage}${result.targetPackage}`.padEnd(50) + '│']
: []),
'│ │',
'│ Run: bash tools/install.sh │',
`│ Or: ${installCommand}`.padEnd(50) + '│',
'│ Or: npm i -g @mosaic/cli@latest │',
'│ │',
'╰─────────────────────────────────────────────────╯',
'',

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/queue",
"version": "0.0.3",
"version": "0.0.2",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",