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
12 changed files with 39 additions and 290 deletions

View File

@@ -1,54 +0,0 @@
# Scratchpad — issue #387 updater simplification + Gitea wrapper repo context
- Objective: simplify updater behavior to `@mosaic/mosaic` only, and fix Gitea wrapper scripts so merge/CI/issue operations work reliably when tea needs explicit repo/login context.
- Scope:
- `packages/mosaic/src/runtime/update-checker.ts`
- `packages/mosaic/__tests__/update-checker.test.ts`
- any package metadata/version bumps needed
- repo-source git wrappers under `packages/mosaic/framework/tools/git/`
- Constraints:
- Jason approved breaking changes; legacy `@mosaic/cli` support is out of scope.
- Keep changes focused and mergeable.
- Acceptance:
- updater only targets `@mosaic/mosaic`
- wrapper path works on Gitea in this environment without manual repo guessing
- PR merges squash-only after green CI
## Progress
### 2026-04-04
#### Update Checker Simplification (DONE)
- Removed `LEGACY_PKG`, `INSTALLED_PACKAGE_ORDER` constants — only `PKG = '@mosaic/mosaic'` remains
- `getInstalledVersion()` — removed loop over multiple packages, only checks `@mosaic/mosaic`
- `getLatestVersion()` — removed `installedPackage` parameter and candidate iteration; single query for `@mosaic/mosaic`
- `checkForUpdate()` — removed `currentPackage`-based cache key comparison; cache is now package-agnostic
- `RegistryCache` — removed `currentPackage` and `targetPackage` fields
- `formatUpdateNotice()` — removed `targetChanged` branch (package migration notice no longer relevant)
- All legacy fallback/compatibility logic removed
#### Test Updates (DONE)
- Removed legacy `@mosaic/cli` fallback test
- Removed cache cross-contamination test (was testing legacy→modern package transition)
- Added `does not query legacy @mosaic/cli package` test — asserts no `@mosaic/cli` npm commands are issued
- Added `returns empty result when package is not installed` test
- All 8 tests pass ✅
#### Gitea Wrapper Fixes (DONE)
- `pr-merge.sh`:
- Added `OWNER=$(get_repo_owner)` / `REPO=$(get_repo_name)` before case block
- tea merge command now includes `--repo $OWNER/$REPO --login ${GITEA_LOGIN:-mosaicstack}`
- `issue-close.sh`:
- Added `OWNER=$(get_repo_owner)` / `REPO=$(get_repo_name)` after detect_platform
- Both `tea issue comment` and `tea issue close` now include `--repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}"`
- `pr-ci-wait.sh`: No changes needed — uses curl API calls (not tea), already passes owner/repo correctly
- `detect-platform.sh`: No changes needed — provides the `get_repo_owner`/`get_repo_name` functions used by wrappers
#### Verification
- `vitest run` — 8/8 tests pass
- `tsc --noEmit` — no errors in update-checker.ts (pre-existing workspace dep errors unrelated)
- `eslint` — clean, no warnings

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,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,83 +49,4 @@ describe('formatUpdateNotice', () => {
expect(notice).toContain('0.1.0');
expect(notice).toContain('Update available');
});
it('uses @mosaic/mosaic for installs', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies: {
'@mosaic/mosaic': { version: '0.0.19' },
},
});
}
if (command.includes('view @mosaic/mosaic version')) {
return '0.0.20';
}
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.19');
expect(result.latest).toBe('0.0.20');
expect(result.currentPackage).toBe('@mosaic/mosaic');
expect(result.targetPackage).toBe('@mosaic/mosaic');
expect(notice).toContain('@mosaic/mosaic@latest');
});
it('does not query legacy @mosaic/cli package', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('view @mosaic/cli')) {
throw new Error('Should not query @mosaic/cli');
}
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies: {
'@mosaic/mosaic': { version: '0.0.19' },
},
});
}
if (command.includes('view @mosaic/mosaic version')) {
return '0.0.20';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
expect(result.targetPackage).toBe('@mosaic/mosaic');
expect(result.latest).toBe('0.0.20');
// Verify no @mosaic/cli queries were made
const calls = execSyncMock.mock.calls.map((c: any[]) => c[0] as string);
expect(calls.some((c) => c.includes('@mosaic/cli'))).toBe(false);
});
it('returns empty result when package is not installed', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({ dependencies: {} });
}
if (command.includes('view @mosaic/mosaic version')) {
return '';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
expect(result.current).toBe('');
expect(result.updateAvailable).toBe(false);
});
});

View File

@@ -45,8 +45,6 @@ fi
# Detect platform and close issue
detect_platform
OWNER=$(get_repo_owner)
REPO=$(get_repo_name)
if [[ "$PLATFORM" == "github" ]]; then
if [[ -n "$COMMENT" ]]; then
@@ -56,9 +54,9 @@ if [[ "$PLATFORM" == "github" ]]; then
echo "Closed GitHub issue #$ISSUE_NUMBER"
elif [[ "$PLATFORM" == "gitea" ]]; then
if [[ -n "$COMMENT" ]]; then
tea issue comment "$ISSUE_NUMBER" "$COMMENT" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}"
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
fi
tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}"
tea issue close "$ISSUE_NUMBER"
echo "Closed Gitea issue #$ISSUE_NUMBER"
else
echo "Error: Unknown platform"

View File

@@ -89,8 +89,6 @@ if [[ "$SKIP_QUEUE_GUARD" != true ]]; then
fi
PLATFORM=$(detect_platform)
OWNER=$(get_repo_owner)
REPO=$(get_repo_name)
case "$PLATFORM" in
github)
@@ -99,7 +97,8 @@ case "$PLATFORM" in
eval "$CMD"
;;
gitea)
CMD="tea pr merge $PR_NUMBER --style squash --repo $OWNER/$REPO --login ${GITEA_LOGIN:-mosaicstack}"
# tea pr merge syntax
CMD="tea pr merge $PR_NUMBER --style squash"
# Delete branch after merge if requested
if [[ "$DELETE_BRANCH" == true ]]; then

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,6 +1,6 @@
/**
* Mosaic update checker — compares the installed @mosaic/mosaic package
* against the Gitea npm registry and reports when an upgrade is available.
* Mosaic update checker — compares installed @mosaic/cli version against the
* Gitea npm registry and reports when an upgrade is available.
*
* Used by:
* - CLI startup (non-blocking background check)
@@ -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,7 +36,7 @@ export interface UpdateCheckResult {
// ─── Constants ──────────────────────────────────────────────────────────────
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
const PKG = '@mosaic/mosaic';
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
@@ -158,40 +154,31 @@ function writeCache(entry: RegistryCache): void {
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Get the currently installed @mosaic/mosaic version.
* 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);
if (raw) {
const data = JSON.parse(raw) as {
dependencies?: Record<string, { version?: string }>;
};
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 @mosaic/mosaic version from the Gitea npm registry.
* Fetch the latest published version from the Gitea npm registry.
* Returns empty string on failure.
*/
export function getLatestVersion(): { name: string; version: string } {
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 || PKG}@latest`;
export function getLatestVersion(): string {
return npmExec(`view ${CLI_PKG} version --registry=${REGISTRY}`);
}
/**
@@ -201,45 +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();
if (cached) {
latestInfo = {
name: PKG,
version: cached.latest,
};
latest = cached.latest;
checkedAt = cached.checkedAt;
} else {
latestInfo = getLatestVersion();
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({
latest: latestInfo.version,
checkedAt,
registry: REGISTRY,
});
writeCache({ latest, checkedAt, registry: REGISTRY });
}
} else {
latestInfo = getLatestVersion();
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({
latest: latestInfo.version,
checkedAt,
registry: REGISTRY,
});
writeCache({ latest, checkedAt, registry: REGISTRY });
}
return {
current,
currentPackage: currentInfo.name || PKG,
latest: latestInfo.version,
targetPackage: latestInfo.name || PKG,
updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)),
latest,
updateAvailable: !!(current && latest && semverLt(current, latest)),
checkedAt,
registry: REGISTRY,
};
@@ -251,8 +224,6 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
export function formatUpdateNotice(result: UpdateCheckResult): string {
if (!result.updateAvailable) return '';
const installCommand = getInstallCommand(result);
const lines = [
'',
'╭─────────────────────────────────────────────────╮',
@@ -260,7 +231,7 @@ export function formatUpdateNotice(result: UpdateCheckResult): string {
`│ Update available: ${result.current}${result.latest}`.padEnd(50) + '│',
'│ │',
'│ Run: bash tools/install.sh │',
`│ Or: ${installCommand}`.padEnd(50) + '│',
'│ Or: npm i -g @mosaic/cli@latest │',
'│ │',
'╰─────────────────────────────────────────────────╯',
'',