Compare commits

..

5 Commits

Author SHA1 Message Date
Jarvis
bf9bcba1ac fix(packages): bump db/memory/queue for PGlite + adapter factories
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Same class of bug as #386: source content changed without a version
bump, so the published tarballs on Gitea are stale and missing exports
that the gateway imports at runtime.

- @mosaic/db@0.0.2 registry tarball has no client-pglite.* files —
  gateway crashes with "does not provide an export named createPgliteDb"
  (createPgliteDb was added in fc7fa119 without bumping the version)
- @mosaic/memory@0.0.2 is missing factory.* / types.* on registry
- @mosaic/queue@0.0.2 is missing factory.* / types.* on registry

Bumps:
- @mosaic/db 0.0.2 → 0.0.3
- @mosaic/memory 0.0.2 → 0.0.3
- @mosaic/queue 0.0.2 → 0.0.3
- @mosaic/gateway 0.0.5 → 0.0.6 (required because ^0.0.2 in sub-1.0
  semver only matches 0.0.2 exactly, so gateway must republish with
  ^0.0.3 deps)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:15:59 -05:00
255ba46a4d fix(packages): republish @mosaic/config and bump dependents (#386)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 01:56:57 +00:00
10285933a0 fix: retarget updater to @mosaic/mosaic (#384)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 01:52:30 +00:00
543388e18b fix(mosaic): resolve framework scripts via import.meta.url (#385)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Fixes #383 — resolveTool now uses fileURLToPath(import.meta.url). Adds package.json/framework subpath exports. Bumps @mosaic/mosaic to 0.0.18.
2026-04-05 01:41:46 +00:00
07a1f5d594 Merge pull request 'feat(mosaic): merge @mosaic/cli into @mosaic/mosaic' (#381) from fix/merge-cli-into-mosaic into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 01:11:33 +00:00
13 changed files with 276 additions and 32 deletions

View File

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

View File

@@ -0,0 +1,34 @@
# 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.16",
"version": "0.0.17",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",

View File

@@ -314,7 +314,8 @@ program
.description('Check for and install Mosaic CLI updates')
.option('--check', 'Check only, do not install')
.action(async (opts: { check?: boolean }) => {
const { checkForUpdate, formatUpdateNotice } = await import('@mosaic/mosaic');
const { checkForUpdate, formatUpdateNotice, getInstallCommand } =
await import('@mosaic/mosaic');
const { execSync } = await import('node:child_process');
console.log('Checking for updates…');
@@ -344,7 +345,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('npm install -g @mosaic/cli@latest', {
execSync(getInstallCommand(result), {
stdio: 'inherit',
timeout: 60_000,
});

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,40 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } 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);
@@ -25,6 +58,11 @@ describe('semverLt', () => {
});
describe('formatUpdateNotice', () => {
beforeEach(() => {
execSyncMock.mockReset();
cacheFiles.clear();
});
it('returns empty string when up to date', () => {
const result: UpdateCheckResult = {
current: '1.0.0',
@@ -49,4 +87,109 @@ 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

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

View File

@@ -12,6 +12,7 @@ import {
backgroundUpdateCheck,
checkForUpdate,
formatUpdateNotice,
getInstallCommand,
} from './runtime/update-checker.js';
import { runWizard } from './wizard.js';
import { ClackPrompter } from './prompter/clack-prompter.js';
@@ -354,7 +355,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('npm install -g @mosaic/cli@latest', {
execSync(getInstallCommand(result), {
stdio: 'inherit',
timeout: 60_000,
});

View File

@@ -1 +1,11 @@
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 installed @mosaic/cli version against the
* Mosaic update checker — compares the installed Mosaic package against the
* Gitea npm registry and reports when an upgrade is available.
*
* Used by:
@@ -23,8 +23,12 @@ 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 */
@@ -36,7 +40,9 @@ export interface UpdateCheckResult {
// ─── Constants ──────────────────────────────────────────────────────────────
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
const CLI_PKG = '@mosaic/cli';
const MODERN_PKG = '@mosaic/mosaic';
const LEGACY_PKG = '@mosaic/cli';
const INSTALLED_PACKAGE_ORDER = [MODERN_PKG, LEGACY_PKG] as const;
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
@@ -125,17 +131,20 @@ 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(): RegistryCache | null {
function readCache(currentPackage: string): 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;
@@ -154,10 +163,10 @@ function writeCache(entry: RegistryCache): void {
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Get the currently installed version of @mosaic/cli.
* Returns empty string if not installed.
* Get the currently installed Mosaic package version.
* Prefers the consolidated @mosaic/mosaic package over legacy @mosaic/cli.
*/
export function getInstalledVersion(): string {
export function getInstalledVersion(): { name: string; version: string } {
// Fast path: check via package.json require chain
try {
const raw = npmExec(`ls -g --depth=0 --json 2>/dev/null`, 3000);
@@ -165,20 +174,41 @@ export function getInstalledVersion(): string {
const data = JSON.parse(raw) as {
dependencies?: Record<string, { version?: string }>;
};
return data?.dependencies?.[CLI_PKG]?.version ?? '';
for (const pkg of INSTALLED_PACKAGE_ORDER) {
const version = data?.dependencies?.[pkg]?.version;
if (version) {
return { name: pkg, version };
}
}
}
} catch {
// fall through
}
return '';
return { name: '', version: '' };
}
/**
* 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(): string {
return npmExec(`view ${CLI_PKG} version --registry=${REGISTRY}`);
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`;
}
/**
@@ -188,31 +218,49 @@ export function getLatestVersion(): string {
* Never throws.
*/
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
const current = getInstalledVersion();
const currentInfo = getInstalledVersion();
const current = currentInfo.version;
let latest: string;
let latestInfo: { name: string; version: string };
let checkedAt: string;
if (!options?.skipCache) {
const cached = readCache();
const cached = readCache(currentInfo.name);
if (cached) {
latest = cached.latest;
latestInfo = {
name: cached.targetPackage || MODERN_PKG,
version: cached.latest,
};
checkedAt = cached.checkedAt;
} else {
latest = getLatestVersion();
latestInfo = getLatestVersion(currentInfo.name);
checkedAt = new Date().toISOString();
writeCache({ latest, checkedAt, registry: REGISTRY });
writeCache({
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name,
checkedAt,
registry: REGISTRY,
});
}
} else {
latest = getLatestVersion();
latestInfo = getLatestVersion(currentInfo.name);
checkedAt = new Date().toISOString();
writeCache({ latest, checkedAt, registry: REGISTRY });
writeCache({
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name,
checkedAt,
registry: REGISTRY,
});
}
return {
current,
latest,
updateAvailable: !!(current && latest && semverLt(current, latest)),
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name || MODERN_PKG,
updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)),
checkedAt,
registry: REGISTRY,
};
@@ -224,14 +272,21 @@ 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: npm i -g @mosaic/cli@latest │',
`│ Or: ${installCommand}`.padEnd(50) + '│',
'│ │',
'╰─────────────────────────────────────────────────╯',
'',

View File

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