Compare commits
1 Commits
ebf9517dd0
...
fix/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6d5fe5773 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/gateway",
|
"name": "@mosaic/gateway",
|
||||||
"version": "0.0.6",
|
"version": "0.1.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Gateway Install UX Fixes — 2026-04-04
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
User hit two publish-drift bugs on `mosaic gateway install` (fixed in #386 and
|
|
||||||
#389). On top of those, the install UX itself has three concrete problems:
|
|
||||||
|
|
||||||
1. Admin API token is generated by the bootstrap step and saved to
|
|
||||||
`~/.config/mosaic/gateway/meta.json`, but never shown to the user. There is
|
|
||||||
no way to retrieve it without reading the file or running `mosaic gateway
|
|
||||||
config` afterward.
|
|
||||||
2. When install crashes mid-way (e.g. daemon fails to become healthy), the
|
|
||||||
next run of `mosaic gateway install` prompts "Reinstall? [y/N]". Saying N
|
|
||||||
aborts, leaving the user with a half-configured install and no clear path
|
|
||||||
forward. There is no resume path.
|
|
||||||
3. Health-check failure only prints "Check logs: mosaic gateway logs" — forcing
|
|
||||||
the user to run another command to see the actual error.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Make `mosaic gateway install` a single end-to-end, resumable command that shows
|
|
||||||
the admin token on creation and surfaces errors inline.
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
- `install.ts`: detect partial state (meta exists, no admin token, daemon not
|
|
||||||
running) and skip already-completed phases instead of aborting.
|
|
||||||
- `install.ts`: add a prominent "Admin API Token" banner printed right after
|
|
||||||
bootstrap succeeds. Include copy-now warning.
|
|
||||||
- `install.ts`: on health-check timeout, read the tail of `LOG_FILE` and print
|
|
||||||
the last ~30 non-empty lines before returning.
|
|
||||||
- Keep `mosaic gateway config` as-is (view/edit env vars); this is not the
|
|
||||||
setup wizard.
|
|
||||||
- No new commands. No new flags yet.
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- Readline/stdin piping fragility (pre-existing, not related to user complaint).
|
|
||||||
- Refactor to a phased state machine (overkill for three targeted fixes).
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- Typecheck, lint, format (mandatory gates).
|
|
||||||
- Manual end-to-end: fresh install → confirm token displayed.
|
|
||||||
- Manual resume: delete daemon.pid mid-install → re-run → confirm it resumes.
|
|
||||||
- Manual failure: point ENV_FILE port to a used port → re-run → confirm log
|
|
||||||
tail is printed.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/config",
|
"name": "@mosaic/config",
|
||||||
"version": "0.0.2",
|
"version": "0.0.1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/db",
|
"name": "@mosaic/db",
|
||||||
"version": "0.0.3",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/memory",
|
"name": "@mosaic/memory",
|
||||||
"version": "0.0.3",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
||||||
|
|||||||
@@ -88,18 +88,19 @@ describe('formatUpdateNotice', () => {
|
|||||||
expect(notice).toContain('Update available');
|
expect(notice).toContain('Update available');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses @mosaic/mosaic for installs', async () => {
|
it('uses @mosaic/mosaic hints for modern installs', async () => {
|
||||||
execSyncMock.mockImplementation((command: string) => {
|
execSyncMock.mockImplementation((command: string) => {
|
||||||
if (command.includes('ls -g --depth=0 --json')) {
|
if (command.includes('ls -g --depth=0 --json')) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
dependencies: {
|
dependencies: {
|
||||||
'@mosaic/mosaic': { version: '0.0.19' },
|
'@mosaic/mosaic': { version: '0.0.17' },
|
||||||
|
'@mosaic/cli': { version: '0.0.16' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.includes('view @mosaic/mosaic version')) {
|
if (command.includes('view @mosaic/mosaic version')) {
|
||||||
return '0.0.20';
|
return '0.0.18';
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unexpected command: ${command}`);
|
throw new Error(`Unexpected command: ${command}`);
|
||||||
@@ -109,29 +110,28 @@ describe('formatUpdateNotice', () => {
|
|||||||
const result = checkForUpdate({ skipCache: true });
|
const result = checkForUpdate({ skipCache: true });
|
||||||
const notice = formatUpdateNotice(result);
|
const notice = formatUpdateNotice(result);
|
||||||
|
|
||||||
expect(result.current).toBe('0.0.19');
|
expect(result.current).toBe('0.0.17');
|
||||||
expect(result.latest).toBe('0.0.20');
|
expect(result.latest).toBe('0.0.18');
|
||||||
expect(result.currentPackage).toBe('@mosaic/mosaic');
|
|
||||||
expect(result.targetPackage).toBe('@mosaic/mosaic');
|
|
||||||
expect(notice).toContain('@mosaic/mosaic@latest');
|
expect(notice).toContain('@mosaic/mosaic@latest');
|
||||||
|
expect(notice).not.toContain('@mosaic/cli@latest');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not query legacy @mosaic/cli package', async () => {
|
it('falls back to @mosaic/mosaic for legacy @mosaic/cli installs when cli is unavailable', async () => {
|
||||||
execSyncMock.mockImplementation((command: string) => {
|
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')) {
|
if (command.includes('ls -g --depth=0 --json')) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
dependencies: {
|
dependencies: {
|
||||||
'@mosaic/mosaic': { version: '0.0.19' },
|
'@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')) {
|
if (command.includes('view @mosaic/mosaic version')) {
|
||||||
return '0.0.20';
|
return '0.0.17';
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unexpected command: ${command}`);
|
throw new Error(`Unexpected command: ${command}`);
|
||||||
@@ -139,31 +139,57 @@ describe('formatUpdateNotice', () => {
|
|||||||
|
|
||||||
const { checkForUpdate } = await importUpdateChecker();
|
const { checkForUpdate } = await importUpdateChecker();
|
||||||
const result = checkForUpdate({ skipCache: true });
|
const result = checkForUpdate({ skipCache: true });
|
||||||
|
const notice = formatUpdateNotice(result);
|
||||||
|
|
||||||
expect(result.targetPackage).toBe('@mosaic/mosaic');
|
expect(result.current).toBe('0.0.16');
|
||||||
expect(result.latest).toBe('0.0.20');
|
expect(result.latest).toBe('0.0.17');
|
||||||
// Verify no @mosaic/cli queries were made
|
expect(notice).toContain('@mosaic/mosaic@latest');
|
||||||
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 () => {
|
it('does not reuse a cached modern-package result for a legacy install', async () => {
|
||||||
|
let installedPackage = '@mosaic/mosaic';
|
||||||
|
|
||||||
execSyncMock.mockImplementation((command: string) => {
|
execSyncMock.mockImplementation((command: string) => {
|
||||||
if (command.includes('ls -g --depth=0 --json')) {
|
if (command.includes('ls -g --depth=0 --json')) {
|
||||||
return JSON.stringify({ dependencies: {} });
|
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')) {
|
if (command.includes('view @mosaic/mosaic version')) {
|
||||||
return '';
|
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}`);
|
throw new Error(`Unexpected command: ${command}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { checkForUpdate } = await importUpdateChecker();
|
const { checkForUpdate } = await importUpdateChecker();
|
||||||
const result = checkForUpdate({ skipCache: true });
|
|
||||||
|
|
||||||
expect(result.current).toBe('');
|
const modernResult = checkForUpdate();
|
||||||
expect(result.updateAvailable).toBe(false);
|
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),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ fi
|
|||||||
|
|
||||||
# Detect platform and close issue
|
# Detect platform and close issue
|
||||||
detect_platform
|
detect_platform
|
||||||
OWNER=$(get_repo_owner)
|
|
||||||
REPO=$(get_repo_name)
|
|
||||||
|
|
||||||
if [[ "$PLATFORM" == "github" ]]; then
|
if [[ "$PLATFORM" == "github" ]]; then
|
||||||
if [[ -n "$COMMENT" ]]; then
|
if [[ -n "$COMMENT" ]]; then
|
||||||
@@ -56,9 +54,9 @@ if [[ "$PLATFORM" == "github" ]]; then
|
|||||||
echo "Closed GitHub issue #$ISSUE_NUMBER"
|
echo "Closed GitHub issue #$ISSUE_NUMBER"
|
||||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||||
if [[ -n "$COMMENT" ]]; 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
|
fi
|
||||||
tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}"
|
tea issue close "$ISSUE_NUMBER"
|
||||||
echo "Closed Gitea issue #$ISSUE_NUMBER"
|
echo "Closed Gitea issue #$ISSUE_NUMBER"
|
||||||
else
|
else
|
||||||
echo "Error: Unknown platform"
|
echo "Error: Unknown platform"
|
||||||
|
|||||||
@@ -89,8 +89,6 @@ if [[ "$SKIP_QUEUE_GUARD" != true ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
PLATFORM=$(detect_platform)
|
PLATFORM=$(detect_platform)
|
||||||
OWNER=$(get_repo_owner)
|
|
||||||
REPO=$(get_repo_name)
|
|
||||||
|
|
||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
github)
|
github)
|
||||||
@@ -99,7 +97,8 @@ case "$PLATFORM" in
|
|||||||
eval "$CMD"
|
eval "$CMD"
|
||||||
;;
|
;;
|
||||||
gitea)
|
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
|
# Delete branch after merge if requested
|
||||||
if [[ "$DELETE_BRANCH" == true ]]; then
|
if [[ "$DELETE_BRANCH" == true ]]; then
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/mosaic",
|
"name": "@mosaic/mosaic",
|
||||||
"version": "0.0.20",
|
"version": "0.0.18",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { createInterface } from 'node:readline';
|
import { createInterface } from 'node:readline';
|
||||||
import type { GatewayMeta } from './daemon.js';
|
import type { GatewayMeta } from './daemon.js';
|
||||||
import {
|
import {
|
||||||
ENV_FILE,
|
ENV_FILE,
|
||||||
GATEWAY_HOME,
|
GATEWAY_HOME,
|
||||||
LOG_FILE,
|
|
||||||
ensureDirs,
|
ensureDirs,
|
||||||
getDaemonPid,
|
|
||||||
installGatewayPackage,
|
installGatewayPackage,
|
||||||
readMeta,
|
readMeta,
|
||||||
resolveGatewayEntry,
|
resolveGatewayEntry,
|
||||||
startDaemon,
|
startDaemon,
|
||||||
stopDaemon,
|
|
||||||
waitForHealth,
|
waitForHealth,
|
||||||
writeMeta,
|
writeMeta,
|
||||||
getInstalledGatewayVersion,
|
getInstalledGatewayVersion,
|
||||||
} from './daemon.js';
|
} from './daemon.js';
|
||||||
|
|
||||||
const MOSAIC_CONFIG_FILE = join(GATEWAY_HOME, 'mosaic.config.json');
|
|
||||||
|
|
||||||
interface InstallOpts {
|
interface InstallOpts {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -41,198 +36,30 @@ export async function runInstall(opts: InstallOpts): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOpts): Promise<void> {
|
async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOpts): Promise<void> {
|
||||||
|
// Check existing installation
|
||||||
const existing = readMeta();
|
const existing = readMeta();
|
||||||
const envExists = existsSync(ENV_FILE);
|
if (existing) {
|
||||||
const mosaicConfigExists = existsSync(MOSAIC_CONFIG_FILE);
|
const answer = await prompt(
|
||||||
let hasConfig = envExists && mosaicConfigExists;
|
rl,
|
||||||
let daemonRunning = getDaemonPid() !== null;
|
`Gateway already installed (v${existing.version}). Reinstall? [y/N] `,
|
||||||
const hasAdminToken = Boolean(existing?.adminToken);
|
|
||||||
// `opts.host` already incorporates meta fallback via the parent command
|
|
||||||
// in gateway.ts (resolveOpts). Using it directly also lets a user pass
|
|
||||||
// `--host X` to recover from a previous install that stored a broken
|
|
||||||
// host. We intentionally do not prefer `existing.host` over `opts.host`.
|
|
||||||
const host = opts.host;
|
|
||||||
|
|
||||||
// Corrupt partial state: exactly one of the two config files survived.
|
|
||||||
// This happens when an earlier install was interrupted between writing
|
|
||||||
// .env and mosaic.config.json. Rewriting the missing one would silently
|
|
||||||
// rotate BETTER_AUTH_SECRET or clobber saved DB/Valkey URLs. Refuse to
|
|
||||||
// guess — tell the user how to recover. Check file presence only; do
|
|
||||||
// NOT gate on `existing`, because the installer writes config before
|
|
||||||
// meta, so an interrupted first install has no meta yet.
|
|
||||||
if ((envExists || mosaicConfigExists) && !hasConfig) {
|
|
||||||
console.error('Gateway install is in a corrupt partial state:');
|
|
||||||
console.error(` .env file: ${envExists ? 'present' : 'MISSING'} (${ENV_FILE})`);
|
|
||||||
console.error(
|
|
||||||
` mosaic.config.json: ${mosaicConfigExists ? 'present' : 'MISSING'} (${MOSAIC_CONFIG_FILE})`,
|
|
||||||
);
|
);
|
||||||
console.error('\nRun `mosaic gateway uninstall` to clean up, then re-run install.');
|
if (answer.toLowerCase() !== 'y') {
|
||||||
return;
|
console.log('Aborted.');
|
||||||
}
|
|
||||||
|
|
||||||
// Fully set up already — offer to re-run the config wizard and restart.
|
|
||||||
// The wizard allows changing storage tier / DB URLs, so this can move
|
|
||||||
// the install onto a different data store. We do NOT wipe persisted
|
|
||||||
// local data here — for a true scratch wipe run `mosaic gateway
|
|
||||||
// uninstall` first.
|
|
||||||
let explicitReinstall = false;
|
|
||||||
if (existing && hasConfig && daemonRunning && hasAdminToken) {
|
|
||||||
console.log(`Gateway is already installed and running (v${existing.version}).`);
|
|
||||||
console.log(` Endpoint: http://${existing.host}:${existing.port.toString()}`);
|
|
||||||
console.log(` Status: mosaic gateway status`);
|
|
||||||
console.log();
|
|
||||||
console.log('Re-running the config wizard will:');
|
|
||||||
console.log(' - regenerate .env and mosaic.config.json');
|
|
||||||
console.log(' - restart the daemon');
|
|
||||||
console.log(' - preserve BETTER_AUTH_SECRET (sessions stay valid)');
|
|
||||||
console.log(' - clear the stored admin token (you will re-bootstrap an admin user)');
|
|
||||||
console.log(' - allow changing storage tier / DB URLs (may point at a different data store)');
|
|
||||||
console.log('To wipe persisted data, run `mosaic gateway uninstall` first.');
|
|
||||||
const answer = await prompt(rl, 'Re-run config wizard? [y/N] ');
|
|
||||||
if (answer.trim().toLowerCase() !== 'y') {
|
|
||||||
console.log('Nothing to do.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Fall through. The daemon stop below triggers because hasConfig=false
|
|
||||||
// forces the wizard to re-run.
|
|
||||||
hasConfig = false;
|
|
||||||
explicitReinstall = true;
|
|
||||||
} else if (existing && (hasConfig || daemonRunning)) {
|
|
||||||
// Partial install detected — resume instead of re-prompting the user.
|
|
||||||
console.log('Detected a partial gateway installation — resuming setup.\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are going to (re)write config, the running daemon would end up
|
// Step 1: Install npm package
|
||||||
// serving the old config while health checks and meta point at the new
|
if (!opts.skipInstall) {
|
||||||
// one. Always stop the daemon before writing config.
|
|
||||||
if (!hasConfig && daemonRunning) {
|
|
||||||
console.log('Stopping gateway daemon before writing new config...');
|
|
||||||
try {
|
|
||||||
await stopDaemon();
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
if (/not running/i.test(msg)) {
|
|
||||||
// Raced with daemon exit — fine, proceed.
|
|
||||||
} else {
|
|
||||||
console.error(`Failed to stop running daemon: ${msg}`);
|
|
||||||
console.error('Refusing to rewrite config while an unknown-state daemon is running.');
|
|
||||||
console.error('Stop it manually (mosaic gateway stop) and re-run install.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Re-check — stop may have succeeded but we want to be sure before
|
|
||||||
// writing new config files and starting a fresh process.
|
|
||||||
if (getDaemonPid() !== null) {
|
|
||||||
console.error('Gateway daemon is still running after stop attempt. Aborting.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
daemonRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Install npm package. Always run on first install and on any
|
|
||||||
// resume where the daemon is NOT already running — a prior failure may
|
|
||||||
// have been caused by a broken package version, and the retry should
|
|
||||||
// pick up the latest release. Skip only when resuming while the daemon
|
|
||||||
// is already alive (package must be working to have started).
|
|
||||||
if (!opts.skipInstall && !daemonRunning) {
|
|
||||||
installGatewayPackage();
|
installGatewayPackage();
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
|
|
||||||
// Step 2: Collect configuration (skip if both files already exist).
|
// Step 2: Collect configuration
|
||||||
// On resume, treat the .env file as authoritative for port — but let a
|
|
||||||
// user-supplied non-default `--port` override it so they can recover
|
|
||||||
// from a conflicting saved port the same way `--host` lets them
|
|
||||||
// recover from a bad saved host. `opts.port === 14242` is commander's
|
|
||||||
// default (not explicit user input), so we prefer .env in that case.
|
|
||||||
let port: number;
|
|
||||||
const regeneratedConfig = !hasConfig;
|
|
||||||
if (hasConfig) {
|
|
||||||
const envPort = readPortFromEnv();
|
|
||||||
port = opts.port !== 14242 ? opts.port : (envPort ?? existing?.port ?? opts.port);
|
|
||||||
console.log(`Using existing config at ${ENV_FILE} (port ${port.toString()})`);
|
|
||||||
} else {
|
|
||||||
port = await runConfigWizard(rl, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Write meta.json. Prefer host from existing meta when resuming.
|
|
||||||
let entryPoint: string;
|
|
||||||
try {
|
|
||||||
entryPoint = resolveGatewayEntry();
|
|
||||||
} catch {
|
|
||||||
console.error('Error: Gateway package not found after install.');
|
|
||||||
console.error('Check that @mosaic/gateway installed correctly.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = getInstalledGatewayVersion() ?? 'unknown';
|
|
||||||
// Preserve the admin token only on a pure resume (no config regeneration).
|
|
||||||
// Any time we regenerated config, the wizard may have pointed at a
|
|
||||||
// different storage tier / DB URL, so the old token is unverifiable —
|
|
||||||
// drop it and require re-bootstrap.
|
|
||||||
const preserveToken = !regeneratedConfig && Boolean(existing?.adminToken);
|
|
||||||
const meta: GatewayMeta = {
|
|
||||||
version,
|
|
||||||
installedAt: explicitReinstall
|
|
||||||
? new Date().toISOString()
|
|
||||||
: (existing?.installedAt ?? new Date().toISOString()),
|
|
||||||
entryPoint,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
...(preserveToken && existing?.adminToken ? { adminToken: existing.adminToken } : {}),
|
|
||||||
};
|
|
||||||
writeMeta(meta);
|
|
||||||
|
|
||||||
// Step 4: Start the daemon (idempotent — skip if already running).
|
|
||||||
if (!daemonRunning) {
|
|
||||||
console.log('\nStarting gateway daemon...');
|
|
||||||
try {
|
|
||||||
const pid = startDaemon();
|
|
||||||
console.log(`Gateway started (PID ${pid.toString()})`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
printLogTail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('\nGateway daemon is already running.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Wait for health
|
|
||||||
console.log('Waiting for gateway to become healthy...');
|
|
||||||
const healthy = await waitForHealth(host, port, 30_000);
|
|
||||||
if (!healthy) {
|
|
||||||
console.error('\nGateway did not become healthy within 30 seconds.');
|
|
||||||
printLogTail();
|
|
||||||
console.error('\nFix the underlying error above, then re-run `mosaic gateway install`.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Gateway is healthy.\n');
|
|
||||||
|
|
||||||
// Step 6: Bootstrap — first admin user.
|
|
||||||
await bootstrapFirstUser(rl, host, port, meta);
|
|
||||||
|
|
||||||
console.log('\n─── Installation Complete ───');
|
|
||||||
console.log(` Endpoint: http://${host}:${port.toString()}`);
|
|
||||||
console.log(` Config: ${GATEWAY_HOME}`);
|
|
||||||
console.log(` Logs: mosaic gateway logs`);
|
|
||||||
console.log(` Status: mosaic gateway status`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runConfigWizard(
|
|
||||||
rl: ReturnType<typeof createInterface>,
|
|
||||||
opts: InstallOpts,
|
|
||||||
): Promise<number> {
|
|
||||||
console.log('\n─── Gateway Configuration ───\n');
|
console.log('\n─── Gateway Configuration ───\n');
|
||||||
|
|
||||||
// If a previous .env exists on disk, reuse its BETTER_AUTH_SECRET so
|
// Tier selection
|
||||||
// regenerating config does not silently log out existing users.
|
|
||||||
const preservedAuthSecret = readEnvVarFromFile('BETTER_AUTH_SECRET');
|
|
||||||
if (preservedAuthSecret) {
|
|
||||||
console.log('(Preserving existing BETTER_AUTH_SECRET — auth sessions will remain valid.)\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Storage tier:');
|
console.log('Storage tier:');
|
||||||
console.log(' 1. Local (embedded database, no dependencies)');
|
console.log(' 1. Local (embedded database, no dependencies)');
|
||||||
console.log(' 2. Team (PostgreSQL + Valkey required)');
|
console.log(' 2. Team (PostgreSQL + Valkey required)');
|
||||||
@@ -264,8 +91,10 @@ async function runConfigWizard(
|
|||||||
const corsOrigin =
|
const corsOrigin =
|
||||||
(await prompt(rl, 'CORS origin [http://localhost:3000]: ')) || 'http://localhost:3000';
|
(await prompt(rl, 'CORS origin [http://localhost:3000]: ')) || 'http://localhost:3000';
|
||||||
|
|
||||||
const authSecret = preservedAuthSecret ?? randomBytes(32).toString('hex');
|
// Generate auth secret
|
||||||
|
const authSecret = randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
// Step 3: Write .env
|
||||||
const envLines = [
|
const envLines = [
|
||||||
`GATEWAY_PORT=${port.toString()}`,
|
`GATEWAY_PORT=${port.toString()}`,
|
||||||
`BETTER_AUTH_SECRET=${authSecret}`,
|
`BETTER_AUTH_SECRET=${authSecret}`,
|
||||||
@@ -287,6 +116,7 @@ async function runConfigWizard(
|
|||||||
writeFileSync(ENV_FILE, envLines.join('\n') + '\n', { mode: 0o600 });
|
writeFileSync(ENV_FILE, envLines.join('\n') + '\n', { mode: 0o600 });
|
||||||
console.log(`\nConfig written to ${ENV_FILE}`);
|
console.log(`\nConfig written to ${ENV_FILE}`);
|
||||||
|
|
||||||
|
// Step 3b: Write mosaic.config.json
|
||||||
const mosaicConfig =
|
const mosaicConfig =
|
||||||
tier === 'local'
|
tier === 'local'
|
||||||
? {
|
? {
|
||||||
@@ -302,81 +132,66 @@ async function runConfigWizard(
|
|||||||
memory: { type: 'pgvector' },
|
memory: { type: 'pgvector' },
|
||||||
};
|
};
|
||||||
|
|
||||||
writeFileSync(MOSAIC_CONFIG_FILE, JSON.stringify(mosaicConfig, null, 2) + '\n', { mode: 0o600 });
|
const configFile = join(GATEWAY_HOME, 'mosaic.config.json');
|
||||||
console.log(`Config written to ${MOSAIC_CONFIG_FILE}`);
|
writeFileSync(configFile, JSON.stringify(mosaicConfig, null, 2) + '\n', { mode: 0o600 });
|
||||||
|
console.log(`Config written to ${configFile}`);
|
||||||
|
|
||||||
return port;
|
// Step 4: Write meta.json
|
||||||
}
|
let entryPoint: string;
|
||||||
|
|
||||||
function readEnvVarFromFile(key: string): string | null {
|
|
||||||
if (!existsSync(ENV_FILE)) return null;
|
|
||||||
try {
|
try {
|
||||||
for (const line of readFileSync(ENV_FILE, 'utf-8').split('\n')) {
|
entryPoint = resolveGatewayEntry();
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
||||||
const eqIdx = trimmed.indexOf('=');
|
|
||||||
if (eqIdx <= 0) continue;
|
|
||||||
if (trimmed.slice(0, eqIdx) !== key) continue;
|
|
||||||
return trimmed.slice(eqIdx + 1);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
console.error('Error: Gateway package not found after install.');
|
||||||
}
|
console.error('Check that @mosaic/gateway installed correctly.');
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPortFromEnv(): number | null {
|
|
||||||
const raw = readEnvVarFromFile('GATEWAY_PORT');
|
|
||||||
if (raw === null) return null;
|
|
||||||
const parsed = parseInt(raw, 10);
|
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function printLogTail(maxLines = 30): void {
|
|
||||||
if (!existsSync(LOG_FILE)) {
|
|
||||||
console.error(`(no log file at ${LOG_FILE})`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const lines = readFileSync(LOG_FILE, 'utf-8')
|
|
||||||
.split('\n')
|
|
||||||
.filter((l) => l.trim().length > 0);
|
|
||||||
const tail = lines.slice(-maxLines);
|
|
||||||
if (tail.length === 0) {
|
|
||||||
console.error('(log file is empty)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error(`\n─── Last ${tail.length.toString()} log lines (${LOG_FILE}) ───`);
|
|
||||||
for (const line of tail) console.error(line);
|
|
||||||
console.error('─────────────────────────────────────────────');
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Could not read log file: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printAdminTokenBanner(token: string): void {
|
const version = getInstalledGatewayVersion() ?? 'unknown';
|
||||||
const border = '═'.repeat(68);
|
|
||||||
console.log();
|
const meta = {
|
||||||
console.log(border);
|
version,
|
||||||
console.log(' Admin API Token');
|
installedAt: new Date().toISOString(),
|
||||||
console.log(border);
|
entryPoint,
|
||||||
console.log();
|
host: opts.host,
|
||||||
console.log(` ${token}`);
|
port,
|
||||||
console.log();
|
};
|
||||||
console.log(' Save this token now — it will not be shown again in full.');
|
writeMeta(meta);
|
||||||
console.log(' It is stored (read-only) at:');
|
|
||||||
console.log(` ${join(GATEWAY_HOME, 'meta.json')}`);
|
// Step 5: Start the daemon
|
||||||
console.log();
|
console.log('\nStarting gateway daemon...');
|
||||||
console.log(' Use it with admin endpoints, e.g.:');
|
try {
|
||||||
console.log(` mosaic gateway --token <token> status`);
|
const pid = startDaemon();
|
||||||
console.log(border);
|
console.log(`Gateway started (PID ${pid.toString()})`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Wait for health
|
||||||
|
console.log('Waiting for gateway to become healthy...');
|
||||||
|
const healthy = await waitForHealth(opts.host, port, 30_000);
|
||||||
|
if (!healthy) {
|
||||||
|
console.error('Gateway did not become healthy within 30 seconds.');
|
||||||
|
console.error(`Check logs: mosaic gateway logs`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Gateway is healthy.\n');
|
||||||
|
|
||||||
|
// Step 7: Bootstrap — first user setup
|
||||||
|
await bootstrapFirstUser(rl, opts.host, port, meta);
|
||||||
|
|
||||||
|
console.log('\n─── Installation Complete ───');
|
||||||
|
console.log(` Endpoint: http://${opts.host}:${port.toString()}`);
|
||||||
|
console.log(` Config: ${GATEWAY_HOME}`);
|
||||||
|
console.log(` Logs: mosaic gateway logs`);
|
||||||
|
console.log(` Status: mosaic gateway status`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrapFirstUser(
|
async function bootstrapFirstUser(
|
||||||
rl: ReturnType<typeof createInterface>,
|
rl: ReturnType<typeof createInterface>,
|
||||||
host: string,
|
host: string,
|
||||||
port: number,
|
port: number,
|
||||||
meta: GatewayMeta,
|
meta: Omit<GatewayMeta, 'adminToken'> & { adminToken?: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const baseUrl = `http://${host}:${port.toString()}`;
|
const baseUrl = `http://${host}:${port.toString()}`;
|
||||||
|
|
||||||
@@ -386,12 +201,7 @@ async function bootstrapFirstUser(
|
|||||||
|
|
||||||
const status = (await statusRes.json()) as { needsSetup: boolean };
|
const status = (await statusRes.json()) as { needsSetup: boolean };
|
||||||
if (!status.needsSetup) {
|
if (!status.needsSetup) {
|
||||||
if (meta.adminToken) {
|
console.log('Admin user already exists — skipping setup.');
|
||||||
console.log('Admin user already exists (token on file).');
|
|
||||||
} else {
|
|
||||||
console.log('Admin user already exists — skipping setup.');
|
|
||||||
console.log('(No admin token on file — sign in via the web UI to manage tokens.)');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -437,12 +247,12 @@ async function bootstrapFirstUser(
|
|||||||
token: { plaintext: string };
|
token: { plaintext: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Persist the token so future CLI calls can authenticate automatically.
|
// Save admin token to meta
|
||||||
meta.adminToken = result.token.plaintext;
|
meta.adminToken = result.token.plaintext;
|
||||||
writeMeta(meta);
|
writeMeta(meta as GatewayMeta);
|
||||||
|
|
||||||
console.log(`\nAdmin user created: ${result.user.email}`);
|
console.log(`\nAdmin user created: ${result.user.email}`);
|
||||||
printAdminTokenBanner(result.token.plaintext);
|
console.log('Admin API token saved to gateway config.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Bootstrap error: ${err instanceof Error ? err.message : String(err)}`);
|
console.error(`Bootstrap error: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Mosaic update checker — compares the installed @mosaic/mosaic package
|
* Mosaic update checker — compares the installed Mosaic package against the
|
||||||
* against the Gitea npm registry and reports when an upgrade is available.
|
* Gitea npm registry and reports when an upgrade is available.
|
||||||
*
|
*
|
||||||
* Used by:
|
* Used by:
|
||||||
* - CLI startup (non-blocking background check)
|
* - CLI startup (non-blocking background check)
|
||||||
@@ -40,7 +40,9 @@ export interface UpdateCheckResult {
|
|||||||
// ─── Constants ──────────────────────────────────────────────────────────────
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
|
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
|
||||||
const PKG = '@mosaic/mosaic';
|
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_DIR = join(homedir(), '.cache', 'mosaic');
|
||||||
const CACHE_FILE = join(CACHE_DIR, 'update-check.json');
|
const CACHE_FILE = join(CACHE_DIR, 'update-check.json');
|
||||||
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
@@ -129,17 +131,20 @@ export function semverLt(a: string, b: string): boolean {
|
|||||||
/** Cache stores only the latest registry version (the expensive network call).
|
/** Cache stores only the latest registry version (the expensive network call).
|
||||||
* The installed version is always checked fresh — it's a local `npm ls`. */
|
* The installed version is always checked fresh — it's a local `npm ls`. */
|
||||||
interface RegistryCache {
|
interface RegistryCache {
|
||||||
|
currentPackage?: string;
|
||||||
latest: string;
|
latest: string;
|
||||||
|
targetPackage?: string;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
registry: string;
|
registry: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCache(): RegistryCache | null {
|
function readCache(currentPackage: string): 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 RegistryCache;
|
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;
|
||||||
|
if ((raw.currentPackage || '') !== currentPackage) return null;
|
||||||
return raw;
|
return raw;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -158,18 +163,22 @@ function writeCache(entry: RegistryCache): void {
|
|||||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the currently installed @mosaic/mosaic version.
|
* Get the currently installed Mosaic package version.
|
||||||
|
* Prefers the consolidated @mosaic/mosaic package over legacy @mosaic/cli.
|
||||||
*/
|
*/
|
||||||
export function getInstalledVersion(): { name: string; version: string } {
|
export function getInstalledVersion(): { name: string; version: string } {
|
||||||
|
// Fast path: check via package.json require chain
|
||||||
try {
|
try {
|
||||||
const raw = npmExec(`ls -g --depth=0 --json 2>/dev/null`, 3000);
|
const raw = npmExec(`ls -g --depth=0 --json 2>/dev/null`, 3000);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const data = JSON.parse(raw) as {
|
const data = JSON.parse(raw) as {
|
||||||
dependencies?: Record<string, { version?: string }>;
|
dependencies?: Record<string, { version?: string }>;
|
||||||
};
|
};
|
||||||
const version = data?.dependencies?.[PKG]?.version;
|
for (const pkg of INSTALLED_PACKAGE_ORDER) {
|
||||||
if (version) {
|
const version = data?.dependencies?.[pkg]?.version;
|
||||||
return { name: PKG, version };
|
if (version) {
|
||||||
|
return { name: pkg, version };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -179,19 +188,27 @@ export function getInstalledVersion(): { name: string; version: string } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the latest published @mosaic/mosaic version from the Gitea npm registry.
|
* 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.
|
* Returns empty string on failure.
|
||||||
*/
|
*/
|
||||||
export function getLatestVersion(): { name: string; version: string } {
|
export function getLatestVersion(installedPackage = ''): { name: string; version: string } {
|
||||||
const version = npmExec(`view ${PKG} version --registry=${REGISTRY}`);
|
const candidates =
|
||||||
if (version) {
|
installedPackage === LEGACY_PKG ? [LEGACY_PKG, MODERN_PKG] : [MODERN_PKG, LEGACY_PKG];
|
||||||
return { name: PKG, version };
|
|
||||||
|
for (const pkg of candidates) {
|
||||||
|
const version = npmExec(`view ${pkg} version --registry=${REGISTRY}`);
|
||||||
|
if (version) {
|
||||||
|
return { name: pkg, version };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name: '', version: '' };
|
return { name: '', version: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInstallCommand(result: Pick<UpdateCheckResult, 'targetPackage'>): string {
|
export function getInstallCommand(result: Pick<UpdateCheckResult, 'targetPackage'>): string {
|
||||||
return `npm i -g ${result.targetPackage || PKG}@latest`;
|
return `npm i -g ${result.targetPackage || MODERN_PKG}@latest`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,27 +225,31 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
|
|||||||
let checkedAt: string;
|
let checkedAt: string;
|
||||||
|
|
||||||
if (!options?.skipCache) {
|
if (!options?.skipCache) {
|
||||||
const cached = readCache();
|
const cached = readCache(currentInfo.name);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
latestInfo = {
|
latestInfo = {
|
||||||
name: PKG,
|
name: cached.targetPackage || MODERN_PKG,
|
||||||
version: cached.latest,
|
version: cached.latest,
|
||||||
};
|
};
|
||||||
checkedAt = cached.checkedAt;
|
checkedAt = cached.checkedAt;
|
||||||
} else {
|
} else {
|
||||||
latestInfo = getLatestVersion();
|
latestInfo = getLatestVersion(currentInfo.name);
|
||||||
checkedAt = new Date().toISOString();
|
checkedAt = new Date().toISOString();
|
||||||
writeCache({
|
writeCache({
|
||||||
|
currentPackage: currentInfo.name,
|
||||||
latest: latestInfo.version,
|
latest: latestInfo.version,
|
||||||
|
targetPackage: latestInfo.name,
|
||||||
checkedAt,
|
checkedAt,
|
||||||
registry: REGISTRY,
|
registry: REGISTRY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
latestInfo = getLatestVersion();
|
latestInfo = getLatestVersion(currentInfo.name);
|
||||||
checkedAt = new Date().toISOString();
|
checkedAt = new Date().toISOString();
|
||||||
writeCache({
|
writeCache({
|
||||||
|
currentPackage: currentInfo.name,
|
||||||
latest: latestInfo.version,
|
latest: latestInfo.version,
|
||||||
|
targetPackage: latestInfo.name,
|
||||||
checkedAt,
|
checkedAt,
|
||||||
registry: REGISTRY,
|
registry: REGISTRY,
|
||||||
});
|
});
|
||||||
@@ -236,9 +257,9 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
currentPackage: currentInfo.name || PKG,
|
currentPackage: currentInfo.name,
|
||||||
latest: latestInfo.version,
|
latest: latestInfo.version,
|
||||||
targetPackage: latestInfo.name || PKG,
|
targetPackage: latestInfo.name || MODERN_PKG,
|
||||||
updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)),
|
updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)),
|
||||||
checkedAt,
|
checkedAt,
|
||||||
registry: REGISTRY,
|
registry: REGISTRY,
|
||||||
@@ -252,12 +273,17 @@ export function formatUpdateNotice(result: UpdateCheckResult): string {
|
|||||||
if (!result.updateAvailable) return '';
|
if (!result.updateAvailable) return '';
|
||||||
|
|
||||||
const installCommand = getInstallCommand(result);
|
const installCommand = getInstallCommand(result);
|
||||||
|
const targetChanged =
|
||||||
|
result.currentPackage && result.targetPackage && result.currentPackage !== result.targetPackage;
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
'',
|
'',
|
||||||
'╭─────────────────────────────────────────────────╮',
|
'╭─────────────────────────────────────────────────╮',
|
||||||
'│ │',
|
'│ │',
|
||||||
`│ Update available: ${result.current} → ${result.latest}`.padEnd(50) + '│',
|
`│ Update available: ${result.current} → ${result.latest}`.padEnd(50) + '│',
|
||||||
|
...(targetChanged
|
||||||
|
? [`│ Package target: ${result.currentPackage} → ${result.targetPackage}`.padEnd(50) + '│']
|
||||||
|
: []),
|
||||||
'│ │',
|
'│ │',
|
||||||
'│ Run: bash tools/install.sh │',
|
'│ Run: bash tools/install.sh │',
|
||||||
`│ Or: ${installCommand}`.padEnd(50) + '│',
|
`│ Or: ${installCommand}`.padEnd(50) + '│',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/queue",
|
"name": "@mosaic/queue",
|
||||||
"version": "0.0.3",
|
"version": "0.0.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
|
||||||
|
|||||||
Reference in New Issue
Block a user