Compare commits
4 Commits
fix/build-
...
1ad0175ec1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ad0175ec1 | ||
|
|
de12221d47 | ||
|
|
1d731fd3f3 | ||
|
|
455b35c755 |
@@ -27,6 +27,7 @@ export default tseslint.config(
|
|||||||
'apps/web/e2e/*.ts',
|
'apps/web/e2e/*.ts',
|
||||||
'apps/web/e2e/helpers/*.ts',
|
'apps/web/e2e/helpers/*.ts',
|
||||||
'apps/web/playwright.config.ts',
|
'apps/web/playwright.config.ts',
|
||||||
|
'packages/mosaic/__tests__/*.ts',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import { registerPrdyCommand } from './commands/prdy.js';
|
|||||||
const _require = createRequire(import.meta.url);
|
const _require = createRequire(import.meta.url);
|
||||||
const CLI_VERSION: string = (_require('../package.json') as { version: string }).version;
|
const CLI_VERSION: string = (_require('../package.json') as { version: string }).version;
|
||||||
|
|
||||||
|
// Fire-and-forget update check at startup (non-blocking, cached 1h)
|
||||||
|
try {
|
||||||
|
const { backgroundUpdateCheck } = await import('@mosaic/mosaic');
|
||||||
|
backgroundUpdateCheck();
|
||||||
|
} catch {
|
||||||
|
// Silently ignore — update check is best-effort
|
||||||
|
}
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
program.name('mosaic').description('Mosaic Stack CLI').version(CLI_VERSION);
|
program.name('mosaic').description('Mosaic Stack CLI').version(CLI_VERSION);
|
||||||
@@ -297,6 +305,54 @@ if (qrCmd !== undefined) {
|
|||||||
program.addCommand(qrCmd as unknown as Command);
|
program.addCommand(qrCmd as unknown as Command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── update ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('update')
|
||||||
|
.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 { execSync } = await import('node:child_process');
|
||||||
|
|
||||||
|
console.log('Checking for updates…');
|
||||||
|
const result = checkForUpdate({ skipCache: true });
|
||||||
|
|
||||||
|
if (!result.latest) {
|
||||||
|
console.error('Could not reach the Mosaic registry.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Installed: ${result.current || '(none)'}`);
|
||||||
|
console.log(` Latest: ${result.latest}`);
|
||||||
|
|
||||||
|
if (!result.updateAvailable) {
|
||||||
|
console.log('\n✔ Up to date.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notice = formatUpdateNotice(result);
|
||||||
|
if (notice) console.log(notice);
|
||||||
|
|
||||||
|
if (opts.check) {
|
||||||
|
process.exit(2); // Signal to callers that an update exists
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Installing update…');
|
||||||
|
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', {
|
||||||
|
stdio: 'inherit',
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
console.log('\n✔ Updated successfully.');
|
||||||
|
} catch {
|
||||||
|
console.error('\nUpdate failed. Try manually: bash tools/install.sh');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ─── wizard ─────────────────────────────────────────────────────────────
|
// ─── wizard ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
program
|
program
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ describe('Full Wizard (headless)', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-wizard-test-'));
|
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-wizard-test-'));
|
||||||
// Copy templates to tmp dir
|
// Copy templates to tmp dir — templates live under framework/ after monorepo migration
|
||||||
const templatesDir = join(repoRoot, 'templates');
|
const candidates = [join(repoRoot, 'framework', 'templates'), join(repoRoot, 'templates')];
|
||||||
if (existsSync(templatesDir)) {
|
for (const templatesDir of candidates) {
|
||||||
cpSync(templatesDir, join(tmpDir, 'templates'), { recursive: true });
|
if (existsSync(templatesDir)) {
|
||||||
|
cpSync(templatesDir, join(tmpDir, 'templates'), { recursive: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
52
packages/mosaic/__tests__/update-checker.test.ts
Normal file
52
packages/mosaic/__tests__/update-checker.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { semverLt, formatUpdateNotice } from '../src/runtime/update-checker.js';
|
||||||
|
import type { UpdateCheckResult } from '../src/runtime/update-checker.js';
|
||||||
|
|
||||||
|
describe('semverLt', () => {
|
||||||
|
it('returns true when a < b', () => {
|
||||||
|
expect(semverLt('0.0.1', '0.0.2')).toBe(true);
|
||||||
|
expect(semverLt('0.1.0', '0.2.0')).toBe(true);
|
||||||
|
expect(semverLt('1.0.0', '2.0.0')).toBe(true);
|
||||||
|
expect(semverLt('0.0.1-alpha.1', '0.0.1-alpha.2')).toBe(true);
|
||||||
|
expect(semverLt('0.0.1-alpha.1', '0.0.1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when a >= b', () => {
|
||||||
|
expect(semverLt('0.0.2', '0.0.1')).toBe(false);
|
||||||
|
expect(semverLt('1.0.0', '1.0.0')).toBe(false);
|
||||||
|
expect(semverLt('2.0.0', '1.0.0')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for empty strings', () => {
|
||||||
|
expect(semverLt('', '1.0.0')).toBe(false);
|
||||||
|
expect(semverLt('1.0.0', '')).toBe(false);
|
||||||
|
expect(semverLt('', '')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatUpdateNotice', () => {
|
||||||
|
it('returns empty string when up to date', () => {
|
||||||
|
const result: UpdateCheckResult = {
|
||||||
|
current: '1.0.0',
|
||||||
|
latest: '1.0.0',
|
||||||
|
updateAvailable: false,
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
registry: 'https://example.com',
|
||||||
|
};
|
||||||
|
expect(formatUpdateNotice(result)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a notice when update is available', () => {
|
||||||
|
const result: UpdateCheckResult = {
|
||||||
|
current: '0.0.1',
|
||||||
|
latest: '0.1.0',
|
||||||
|
updateAvailable: true,
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
registry: 'https://example.com',
|
||||||
|
};
|
||||||
|
const notice = formatUpdateNotice(result);
|
||||||
|
expect(notice).toContain('0.0.1');
|
||||||
|
expect(notice).toContain('0.1.0');
|
||||||
|
expect(notice).toContain('Update available');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -80,7 +80,7 @@ USAGE
|
|||||||
check_mosaic_home() {
|
check_mosaic_home() {
|
||||||
if [[ ! -d "$MOSAIC_HOME" ]]; then
|
if [[ ! -d "$MOSAIC_HOME" ]]; then
|
||||||
echo "[mosaic] ERROR: ~/.config/mosaic not found." >&2
|
echo "[mosaic] ERROR: ~/.config/mosaic not found." >&2
|
||||||
echo "[mosaic] Install with: curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh" >&2
|
echo "[mosaic] Install with: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ check_mosaic_home() {
|
|||||||
check_agents_md() {
|
check_agents_md() {
|
||||||
if [[ ! -f "$MOSAIC_HOME/AGENTS.md" ]]; then
|
if [[ ! -f "$MOSAIC_HOME/AGENTS.md" ]]; then
|
||||||
echo "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." >&2
|
echo "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." >&2
|
||||||
echo "[mosaic] Re-run the installer: cd ~/src/mosaic-bootstrap && bash install.sh" >&2
|
echo "[mosaic] Re-run the installer: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ set -euo pipefail
|
|||||||
# mosaic-release-upgrade --ref v0.2.0 --overwrite --yes
|
# mosaic-release-upgrade --ref v0.2.0 --overwrite --yes
|
||||||
|
|
||||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
REMOTE_SCRIPT_URL="${MOSAIC_REMOTE_INSTALL_URL:-https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh}"
|
REMOTE_SCRIPT_URL="${MOSAIC_REMOTE_INSTALL_URL:-https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh}"
|
||||||
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
|
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
|
||||||
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" # keep|overwrite
|
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" # keep|overwrite
|
||||||
YES=false
|
YES=false
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ $MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:U
|
|||||||
$RemoteInstallerUrl = if ($env:MOSAIC_REMOTE_INSTALL_URL) {
|
$RemoteInstallerUrl = if ($env:MOSAIC_REMOTE_INSTALL_URL) {
|
||||||
$env:MOSAIC_REMOTE_INSTALL_URL
|
$env:MOSAIC_REMOTE_INSTALL_URL
|
||||||
} else {
|
} else {
|
||||||
"https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1"
|
"https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
$installMode = if ($Overwrite) { "overwrite" } elseif ($Keep) { "keep" } elseif ($env:MOSAIC_INSTALL_MODE) { $env:MOSAIC_INSTALL_MODE } else { "keep" }
|
$installMode = if ($Overwrite) { "overwrite" } elseif ($Keep) { "keep" } elseif ($env:MOSAIC_INSTALL_MODE) { $env:MOSAIC_INSTALL_MODE } else { "keep" }
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Options:
|
|||||||
function Assert-MosaicHome {
|
function Assert-MosaicHome {
|
||||||
if (-not (Test-Path $MosaicHome)) {
|
if (-not (Test-Path $MosaicHome)) {
|
||||||
Write-Host "[mosaic] ERROR: ~/.config/mosaic not found." -ForegroundColor Red
|
Write-Host "[mosaic] ERROR: ~/.config/mosaic not found." -ForegroundColor Red
|
||||||
Write-Host "[mosaic] Install with: irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex"
|
Write-Host "[mosaic] Install with: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,41 @@
|
|||||||
# Mosaic Agent Framework
|
# Mosaic Agent Framework
|
||||||
|
|
||||||
Universal agent standards layer for Claude Code, Codex, and OpenCode.
|
Universal agent standards layer for Claude Code, Codex, OpenCode, and Pi.
|
||||||
|
|
||||||
One config, every runtime, same standards.
|
One config, every runtime, same standards.
|
||||||
|
|
||||||
> **This repository is a generic framework baseline.** No personal data, credentials, user-specific preferences, or machine-specific paths should be committed. All personalization happens at install time via `mosaic init` or by editing files in `~/.config/mosaic/` after installation.
|
> **This is the framework component of [mosaic-stack](https://git.mosaicstack.dev/mosaic/mosaic-stack).** No personal data, credentials, user-specific preferences, or machine-specific paths should be committed. All personalization happens at install time via `mosaic init` or by editing files in `~/.config/mosaic/` after installation.
|
||||||
|
|
||||||
## Quick Install
|
## Quick Install
|
||||||
|
|
||||||
### Mac / Linux
|
### Mac / Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows (PowerShell)
|
### Windows (PowerShell)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex
|
# PowerShell installer coming soon — use WSL + the bash installer above.
|
||||||
```
|
```
|
||||||
|
|
||||||
### From Source (any platform)
|
### From Source (any platform)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.mosaicstack.dev/mosaic/bootstrap.git ~/src/mosaic-bootstrap
|
git clone git@git.mosaicstack.dev:mosaic/mosaic-stack.git ~/src/mosaic-stack
|
||||||
cd ~/src/mosaic-bootstrap && bash install.sh
|
cd ~/src/mosaic-stack && bash tools/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
If Node.js 18+ is available, the remote installer automatically uses the TypeScript wizard instead of the bash installer for a richer setup experience.
|
The installer:
|
||||||
|
|
||||||
The installer will:
|
- Downloads the framework from the monorepo archive
|
||||||
|
- Installs it to `~/.config/mosaic/`
|
||||||
- Install the framework to `~/.config/mosaic/`
|
- Installs `@mosaic/cli` globally via npm (TUI, gateway client, wizard)
|
||||||
- Add `~/.config/mosaic/bin` to your PATH
|
- Adds `~/.config/mosaic/bin` to your PATH
|
||||||
- Sync runtime adapters and skills
|
- Syncs runtime adapters and skills
|
||||||
- Install and configure sequential-thinking MCP (hard requirement)
|
- Runs a health audit
|
||||||
- Run a health audit
|
- Detects existing installs and preserves local files (SOUL.md, USER.md, etc.)
|
||||||
- Detect existing installs and prompt to keep or overwrite local files
|
|
||||||
- Prompt you to run `mosaic init` to set up your agent identity
|
|
||||||
|
|
||||||
## First Run
|
## First Run
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ The wizard configures three files loaded into every agent session:
|
|||||||
- `USER.md` — your user profile (name, timezone, accessibility, preferences)
|
- `USER.md` — your user profile (name, timezone, accessibility, preferences)
|
||||||
- `TOOLS.md` — machine-level tool reference (git providers, credentials, CLI patterns)
|
- `TOOLS.md` — machine-level tool reference (git providers, credentials, CLI patterns)
|
||||||
|
|
||||||
It also detects installed runtimes (Claude, Codex, OpenCode), configures sequential-thinking MCP, and offers curated skill selection from 8 categories.
|
It also detects installed runtimes (Claude, Codex, OpenCode, Pi), configures sequential-thinking MCP, and offers curated skill selection from 8 categories.
|
||||||
|
|
||||||
### Non-Interactive Mode
|
### Non-Interactive Mode
|
||||||
|
|
||||||
@@ -77,9 +75,12 @@ If Node.js is unavailable, `mosaic init` falls back to the bash-based `mosaic-in
|
|||||||
## Launching Agent Sessions
|
## Launching Agent Sessions
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
mosaic pi # Launch Pi with full Mosaic injection (recommended)
|
||||||
mosaic claude # Launch Claude Code with full Mosaic injection
|
mosaic claude # Launch Claude Code with full Mosaic injection
|
||||||
mosaic codex # Launch Codex with full Mosaic injection
|
mosaic codex # Launch Codex with full Mosaic injection
|
||||||
mosaic opencode # Launch OpenCode with full Mosaic injection
|
mosaic opencode # Launch OpenCode with full Mosaic injection
|
||||||
|
mosaic yolo claude # Launch Claude in dangerous-permissions mode
|
||||||
|
mosaic yolo pi # Launch Pi in yolo mode
|
||||||
```
|
```
|
||||||
|
|
||||||
The launcher:
|
The launcher:
|
||||||
@@ -100,23 +101,18 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
|||||||
├── USER.md ← User profile and accessibility (generated by mosaic init)
|
├── USER.md ← User profile and accessibility (generated by mosaic init)
|
||||||
├── TOOLS.md ← Machine-level tool reference (generated by mosaic init)
|
├── TOOLS.md ← Machine-level tool reference (generated by mosaic init)
|
||||||
├── STANDARDS.md ← Machine-wide standards
|
├── STANDARDS.md ← Machine-wide standards
|
||||||
├── guides/E2E-DELIVERY.md ← Mandatory E2E software delivery procedure
|
├── guides/ ← Operational guides (E2E delivery, PRD, docs, etc.)
|
||||||
├── guides/PRD.md ← Mandatory PRD requirements gate before coding
|
├── bin/ ← CLI tools (mosaic launcher, mosaic-init, mosaic-doctor, etc.)
|
||||||
├── guides/DOCUMENTATION.md ← Mandatory documentation standard and gates
|
├── tools/ ← Tool suites: git, orchestrator, prdy, quality, etc.
|
||||||
├── bin/ ← CLI tools (mosaic, mosaic-init, mosaic-doctor, etc.)
|
|
||||||
├── dist/ ← Bundled wizard (mosaic-wizard.mjs)
|
|
||||||
├── guides/ ← Operational guides
|
|
||||||
├── tools/ ← Tool suites: git, portainer, authentik, coolify, codex, etc.
|
|
||||||
├── runtime/ ← Runtime adapters + runtime-specific references
|
├── runtime/ ← Runtime adapters + runtime-specific references
|
||||||
│ ├── claude/CLAUDE.md
|
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
|
||||||
│ ├── claude/RUNTIME.md
|
│ ├── codex/ ← instructions.md, RUNTIME.md
|
||||||
│ ├── opencode/AGENTS.md
|
│ ├── opencode/ ← AGENTS.md, RUNTIME.md
|
||||||
│ ├── opencode/RUNTIME.md
|
│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts
|
||||||
│ ├── codex/instructions.md
|
│ └── mcp/ ← MCP server configs
|
||||||
│ ├── codex/RUNTIME.md
|
|
||||||
│ └── mcp/SEQUENTIAL-THINKING.json
|
|
||||||
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
|
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
|
||||||
├── skills-local/ ← Local cross-runtime skills
|
├── skills-local/ ← Local cross-runtime skills
|
||||||
|
├── memory/ ← Persistent agent memory (preserved across upgrades)
|
||||||
└── templates/ ← SOUL.md template, project templates
|
└── templates/ ← SOUL.md template, project templates
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -124,6 +120,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
|||||||
|
|
||||||
| Launch method | Injection mechanism |
|
| Launch method | Injection mechanism |
|
||||||
| ------------------- | ----------------------------------------------------------------------------------------- |
|
| ------------------- | ----------------------------------------------------------------------------------------- |
|
||||||
|
| `mosaic pi` | `--append-system-prompt` with composed runtime contract + skills + extension |
|
||||||
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
|
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
|
||||||
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
|
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
|
||||||
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
|
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
|
||||||
@@ -131,9 +128,6 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
|||||||
| `codex` (direct) | `~/.codex/instructions.md` thin pointer → load AGENTS + runtime reference |
|
| `codex` (direct) | `~/.codex/instructions.md` thin pointer → load AGENTS + runtime reference |
|
||||||
| `opencode` (direct) | `~/.config/opencode/AGENTS.md` thin pointer → load AGENTS + runtime reference |
|
| `opencode` (direct) | `~/.config/opencode/AGENTS.md` thin pointer → load AGENTS + runtime reference |
|
||||||
|
|
||||||
Mosaic `AGENTS.md` enforces loading `guides/E2E-DELIVERY.md` before execution and
|
|
||||||
requires `guides/PRD.md` before coding and `guides/DOCUMENTATION.md` for code/API/auth/infra documentation gates.
|
|
||||||
|
|
||||||
## Management Commands
|
## Management Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -142,126 +136,53 @@ mosaic init # Interactive wizard (or legacy init)
|
|||||||
mosaic doctor # Health audit — detect drift and missing files
|
mosaic doctor # Health audit — detect drift and missing files
|
||||||
mosaic sync # Sync skills from canonical source
|
mosaic sync # Sync skills from canonical source
|
||||||
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
|
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
|
||||||
mosaic upgrade check # Check release upgrade status (no changes)
|
mosaic upgrade # Upgrade installed Mosaic release
|
||||||
mosaic upgrade # Upgrade installed Mosaic release (keeps SOUL.md by default)
|
mosaic upgrade check # Check upgrade status (no changes)
|
||||||
mosaic upgrade --dry-run # Preview release upgrade without changes
|
|
||||||
mosaic upgrade --ref main # Upgrade from a specific branch/tag/commit ref
|
|
||||||
mosaic upgrade --overwrite # Upgrade release and overwrite local files
|
|
||||||
mosaic upgrade project ... # Project file cleanup mode (see below)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upgrading Mosaic Release
|
## Upgrading
|
||||||
|
|
||||||
Upgrade the installed framework in place:
|
Run the installer again — it handles upgrades automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default (safe): keep local SOUL.md, USER.md, TOOLS.md + memory
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
|
||||||
mosaic upgrade
|
|
||||||
|
|
||||||
# Check current/target release info without changing files
|
|
||||||
mosaic upgrade check
|
|
||||||
|
|
||||||
# Non-interactive
|
|
||||||
mosaic upgrade --yes
|
|
||||||
|
|
||||||
# Pull a specific ref
|
|
||||||
mosaic upgrade --ref main
|
|
||||||
|
|
||||||
# Force full overwrite (fresh install semantics)
|
|
||||||
mosaic upgrade --overwrite --yes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`mosaic upgrade` re-runs the remote installer and passes install mode controls (`keep`/`overwrite`).
|
Or from a local checkout:
|
||||||
This is the manual upgrade path today and is suitable for future app-driven update checks.
|
|
||||||
|
|
||||||
## Upgrading Projects
|
|
||||||
|
|
||||||
After centralizing AGENTS.md and SOUL.md, existing projects may have stale files:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Preview what would change across all projects
|
cd ~/src/mosaic-stack && git pull && bash tools/install.sh
|
||||||
mosaic upgrade project --all --dry-run
|
|
||||||
|
|
||||||
# Apply to all projects
|
|
||||||
mosaic upgrade project --all
|
|
||||||
|
|
||||||
# Apply to a specific project
|
|
||||||
mosaic upgrade project ~/src/my-project
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Backward compatibility is preserved for historical usage:
|
The installer preserves local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/` by default.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mosaic upgrade --all # still routes to project-upgrade
|
bash tools/install.sh --check # Version check only
|
||||||
mosaic upgrade ~/src/my-repo # still routes to project-upgrade
|
bash tools/install.sh --framework # Framework only (skip npm CLI)
|
||||||
|
bash tools/install.sh --cli # npm CLI only (skip framework)
|
||||||
|
bash tools/install.sh --ref v1.0 # Install from a specific git ref
|
||||||
```
|
```
|
||||||
|
|
||||||
What it does per project:
|
|
||||||
|
|
||||||
| File | Action |
|
|
||||||
| ----------- | ------------------------------------------------------------- |
|
|
||||||
| `SOUL.md` | Removed — now global at `~/.config/mosaic/SOUL.md` |
|
|
||||||
| `CLAUDE.md` | Replaced with thin pointer to global AGENTS.md |
|
|
||||||
| `AGENTS.md` | Stale load-order sections stripped; project content preserved |
|
|
||||||
|
|
||||||
Backups (`.mosaic-bak`) are created before any modification.
|
|
||||||
|
|
||||||
## Universal Skills
|
## Universal Skills
|
||||||
|
|
||||||
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories (`~/.claude/skills`, `~/.codex/skills`, `~/.config/opencode/skills`).
|
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mosaic sync # Full sync (clone + link)
|
mosaic sync # Full sync (clone + link)
|
||||||
~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only
|
~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime Compatibility
|
## Health Audit
|
||||||
|
|
||||||
The installer pushes thin runtime adapters as regular files (not symlinks):
|
|
||||||
|
|
||||||
- `~/.claude/CLAUDE.md` — pointer to `~/.config/mosaic/AGENTS.md`
|
|
||||||
- `~/.claude/settings.json`, `hooks-config.json`, `context7-integration.md`
|
|
||||||
- `~/.config/opencode/AGENTS.md` — pointer to `~/.config/mosaic/AGENTS.md`
|
|
||||||
- `~/.codex/instructions.md` — pointer to `~/.config/mosaic/AGENTS.md`
|
|
||||||
- `~/.claude/settings.json`, `~/.codex/config.toml`, and `~/.config/opencode/config.json` include sequential-thinking MCP config
|
|
||||||
|
|
||||||
Re-sync manually:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.config/mosaic/bin/mosaic-link-runtime-assets
|
mosaic doctor # Standard audit
|
||||||
|
~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode
|
||||||
```
|
```
|
||||||
|
|
||||||
## MCP Registration
|
## MCP Registration
|
||||||
|
|
||||||
### How MCPs Are Configured in Claude Code
|
|
||||||
|
|
||||||
**MCPs must be registered via `claude mcp add` — not by hand-editing `~/.claude/settings.json`.**
|
|
||||||
|
|
||||||
`settings.json` controls hooks, model, plugins, and allowed commands. The `mcpServers` key in
|
|
||||||
`settings.json` is silently ignored by Claude Code's MCP loader. The correct file is `~/.claude.json`,
|
|
||||||
which is managed by the `claude mcp` CLI.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Register a stdio MCP (user scope = all projects, persists across sessions)
|
|
||||||
claude mcp add --scope user <name> -- npx -y <package>
|
|
||||||
|
|
||||||
# Register an HTTP MCP (e.g. OpenBrain)
|
|
||||||
claude mcp add --scope user --transport http <name> <url> \
|
|
||||||
--header "Authorization: Bearer <token>"
|
|
||||||
|
|
||||||
# List registered MCPs
|
|
||||||
claude mcp list
|
|
||||||
```
|
|
||||||
|
|
||||||
**Scope options:**
|
|
||||||
|
|
||||||
- `--scope user` — writes to `~/.claude.json`, available in all projects (recommended for shared tools)
|
|
||||||
- `--scope project` — writes to `.claude/settings.json` in the project root, committed to the repo
|
|
||||||
- `--scope local` — default, machine-local only, not committed
|
|
||||||
|
|
||||||
**Transport for HTTP MCPs must be `http`** — not `sse`. `type: "sse"` is a deprecated protocol
|
|
||||||
that silently fails to connect against FastMCP streamable HTTP servers.
|
|
||||||
|
|
||||||
### sequential-thinking MCP (Hard Requirement)
|
### sequential-thinking MCP (Hard Requirement)
|
||||||
|
|
||||||
sequential-thinking MCP is required for Mosaic Stack. The installer registers it automatically.
|
sequential-thinking MCP is required for Mosaic Stack. The installer registers it automatically.
|
||||||
@@ -272,74 +193,12 @@ To verify or re-register manually:
|
|||||||
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
|
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
|
||||||
```
|
```
|
||||||
|
|
||||||
### OpenBrain Semantic Memory (Recommended)
|
### Claude Code MCP Registration
|
||||||
|
|
||||||
OpenBrain is the shared cross-agent memory layer. Register once per machine:
|
**MCPs must be registered via `claude mcp add` — not by hand-editing `~/.claude/settings.json`.**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude mcp add --scope user --transport http openbrain https://your-openbrain-host/mcp \
|
claude mcp add --scope user <name> -- npx -y <package>
|
||||||
--header "Authorization: Bearer YOUR_TOKEN"
|
claude mcp add --scope user --transport http <name> <url> --header "Authorization: Bearer <token>"
|
||||||
```
|
claude mcp list
|
||||||
|
|
||||||
See [mosaic/openbrain](https://git.mosaicstack.dev/mosaic/openbrain) for setup and API docs.
|
|
||||||
|
|
||||||
## Bootstrap Any Repo
|
|
||||||
|
|
||||||
Attach any repository to the Mosaic standards layer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mosaic bootstrap /path/to/repo
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates `.mosaic/`, `scripts/agent/`, and an `AGENTS.md` if missing.
|
|
||||||
|
|
||||||
## Quality Rails
|
|
||||||
|
|
||||||
Apply and verify quality templates:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
~/.config/mosaic/bin/mosaic-quality-apply --template typescript-node --target /path/to/repo
|
|
||||||
~/.config/mosaic/bin/mosaic-quality-verify --target /path/to/repo
|
|
||||||
```
|
|
||||||
|
|
||||||
Templates: `typescript-node`, `typescript-nextjs`, `monorepo`
|
|
||||||
|
|
||||||
## Health Audit
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mosaic doctor # Standard audit
|
|
||||||
~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wizard Development
|
|
||||||
|
|
||||||
The installation wizard is a TypeScript project in the root of this repo.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install # Install dependencies
|
|
||||||
pnpm dev # Run wizard from source (tsx)
|
|
||||||
pnpm build # Bundle to dist/mosaic-wizard.mjs
|
|
||||||
pnpm test # Run tests (30 tests, vitest)
|
|
||||||
pnpm typecheck # TypeScript type checking
|
|
||||||
```
|
|
||||||
|
|
||||||
The wizard uses `@clack/prompts` for the interactive TUI and supports `--non-interactive` mode via `HeadlessPrompter` for CI and scripted installs. The bundled output (`dist/mosaic-wizard.mjs`) is committed to the repo so installs work without `node_modules`.
|
|
||||||
|
|
||||||
## Re-installing / Updating
|
|
||||||
|
|
||||||
Pull the latest and re-run the installer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/src/mosaic-bootstrap && git pull && bash install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
If an existing install is detected, the installer prompts for:
|
|
||||||
|
|
||||||
- `keep` (recommended): preserve local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/`
|
|
||||||
- `overwrite`: replace everything in `~/.config/mosaic`
|
|
||||||
|
|
||||||
Or use the one-liner again — it always pulls the latest:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Mosaic Bootstrap — Remote Installer (Windows PowerShell)
|
|
||||||
#
|
|
||||||
# One-liner:
|
|
||||||
# irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex
|
|
||||||
#
|
|
||||||
# Or explicit:
|
|
||||||
# powershell -ExecutionPolicy Bypass -File remote-install.ps1
|
|
||||||
#
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$BootstrapRef = if ($env:MOSAIC_BOOTSTRAP_REF) { $env:MOSAIC_BOOTSTRAP_REF } else { "main" }
|
|
||||||
$ArchiveUrl = "https://git.mosaicstack.dev/mosaic/bootstrap/archive/$BootstrapRef.zip"
|
|
||||||
$WorkDir = Join-Path $env:TEMP "mosaic-bootstrap-$PID"
|
|
||||||
$ZipPath = "$WorkDir.zip"
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-Host "[mosaic] Downloading bootstrap archive (ref: $BootstrapRef)..."
|
|
||||||
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
|
|
||||||
Invoke-WebRequest -Uri $ArchiveUrl -OutFile $ZipPath -UseBasicParsing
|
|
||||||
|
|
||||||
Write-Host "[mosaic] Extracting..."
|
|
||||||
Expand-Archive -Path $ZipPath -DestinationPath $WorkDir -Force
|
|
||||||
|
|
||||||
$InstallScript = Join-Path $WorkDir "bootstrap\install.ps1"
|
|
||||||
if (-not (Test-Path $InstallScript)) {
|
|
||||||
throw "install.ps1 not found in archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "[mosaic] Running install..."
|
|
||||||
& $InstallScript
|
|
||||||
|
|
||||||
Write-Host "[mosaic] Done."
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Write-Host "[mosaic] Cleaning up temporary files..."
|
|
||||||
Remove-Item -Path $ZipPath -Force -ErrorAction SilentlyContinue
|
|
||||||
Remove-Item -Path $WorkDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# Mosaic Bootstrap — Remote Installer (POSIX)
|
|
||||||
#
|
|
||||||
# One-liner:
|
|
||||||
# curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
|
||||||
#
|
|
||||||
# Or with wget:
|
|
||||||
# wget -qO- https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
|
||||||
#
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
|
|
||||||
ARCHIVE_URL="https://git.mosaicstack.dev/mosaic/bootstrap/archive/${BOOTSTRAP_REF}.tar.gz"
|
|
||||||
TMPDIR_BASE="${TMPDIR:-/tmp}"
|
|
||||||
WORK_DIR="$TMPDIR_BASE/mosaic-bootstrap-$$"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$WORK_DIR"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
echo "[mosaic] Downloading bootstrap archive (ref: $BOOTSTRAP_REF)..."
|
|
||||||
|
|
||||||
mkdir -p "$WORK_DIR"
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
curl -sL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
|
||||||
else
|
|
||||||
echo "[mosaic] ERROR: curl or wget required" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$WORK_DIR/bootstrap/install.sh" ]; then
|
|
||||||
echo "[mosaic] ERROR: install.sh not found in archive" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$WORK_DIR/bootstrap"
|
|
||||||
|
|
||||||
# Prefer TypeScript wizard if Node.js 18+ and bundle are available
|
|
||||||
WIZARD_BIN="$WORK_DIR/bootstrap/dist/mosaic-wizard.mjs"
|
|
||||||
if command -v node >/dev/null 2>&1 && [ -f "$WIZARD_BIN" ]; then
|
|
||||||
NODE_MAJOR="$(node -e 'console.log(process.versions.node.split(".")[0])')"
|
|
||||||
if [ "$NODE_MAJOR" -ge 18 ] 2>/dev/null; then
|
|
||||||
if [ -e /dev/tty ]; then
|
|
||||||
echo "[mosaic] Running wizard installer (Node.js $NODE_MAJOR detected)..."
|
|
||||||
node "$WIZARD_BIN" --source-dir "$WORK_DIR/bootstrap" </dev/tty
|
|
||||||
else
|
|
||||||
echo "[mosaic] Running wizard installer in non-interactive mode (no TTY)..."
|
|
||||||
node "$WIZARD_BIN" --source-dir "$WORK_DIR/bootstrap" --non-interactive
|
|
||||||
fi
|
|
||||||
echo "[mosaic] Cleaning up temporary files..."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[mosaic] Running legacy install..."
|
|
||||||
bash install.sh </dev/tty
|
|
||||||
|
|
||||||
echo "[mosaic] Cleaning up temporary files..."
|
|
||||||
# cleanup runs via trap
|
|
||||||
@@ -13,6 +13,15 @@ import type { CommunicationStyle } from './types.js';
|
|||||||
|
|
||||||
export { VERSION, DEFAULT_MOSAIC_HOME };
|
export { VERSION, DEFAULT_MOSAIC_HOME };
|
||||||
export { runWizard } from './wizard.js';
|
export { runWizard } from './wizard.js';
|
||||||
|
export {
|
||||||
|
checkForUpdate,
|
||||||
|
backgroundUpdateCheck,
|
||||||
|
formatUpdateNotice,
|
||||||
|
getInstalledVersion,
|
||||||
|
getLatestVersion,
|
||||||
|
semverLt,
|
||||||
|
} from './runtime/update-checker.js';
|
||||||
|
export type { UpdateCheckResult } from './runtime/update-checker.js';
|
||||||
export { ClackPrompter } from './prompter/clack-prompter.js';
|
export { ClackPrompter } from './prompter/clack-prompter.js';
|
||||||
export { HeadlessPrompter } from './prompter/headless-prompter.js';
|
export { HeadlessPrompter } from './prompter/headless-prompter.js';
|
||||||
export { createConfigService } from './config/config-service.js';
|
export { createConfigService } from './config/config-service.js';
|
||||||
|
|||||||
238
packages/mosaic/src/runtime/update-checker.ts
Normal file
238
packages/mosaic/src/runtime/update-checker.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
* - session-start.sh (via `mosaic update --check`)
|
||||||
|
* - install.sh (direct invocation)
|
||||||
|
*
|
||||||
|
* Design:
|
||||||
|
* - Result is cached to ~/.cache/mosaic/update-check.json for 1 hour
|
||||||
|
* - Network call is fire-and-forget with a 5 s timeout
|
||||||
|
* - Never throws on failure — returns stale/unknown result
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface UpdateCheckResult {
|
||||||
|
/** Currently installed version (empty if not found) */
|
||||||
|
current: string;
|
||||||
|
/** Latest published version (empty if check failed) */
|
||||||
|
latest: string;
|
||||||
|
/** True when a newer version is available */
|
||||||
|
updateAvailable: boolean;
|
||||||
|
/** ISO timestamp of this check */
|
||||||
|
checkedAt: string;
|
||||||
|
/** Where the latest version was resolved from */
|
||||||
|
registry: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
|
||||||
|
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
|
||||||
|
const NETWORK_TIMEOUT_MS = 5_000;
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function npmExec(args: string, timeoutMs = NETWORK_TIMEOUT_MS): string {
|
||||||
|
try {
|
||||||
|
return execSync(`npm ${args}`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: timeoutMs,
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rudimentary semver "a < b" — handles pre-release tags.
|
||||||
|
* Per semver spec: 1.0.0-alpha.1 < 1.0.0 (pre-release has lower precedence).
|
||||||
|
*/
|
||||||
|
export function semverLt(a: string, b: string): boolean {
|
||||||
|
if (!a || !b) return false;
|
||||||
|
|
||||||
|
const stripped = (v: string) => v.replace(/^v/, '');
|
||||||
|
const splitPre = (v: string): [string, string | null] => {
|
||||||
|
const idx = v.indexOf('-');
|
||||||
|
return idx === -1 ? [v, null] : [v.slice(0, idx), v.slice(idx + 1)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sa = stripped(a);
|
||||||
|
const sb = stripped(b);
|
||||||
|
const [coreA, preA] = splitPre(sa);
|
||||||
|
const [coreB, preB] = splitPre(sb);
|
||||||
|
|
||||||
|
// Compare core version (major.minor.patch)
|
||||||
|
const partsA = coreA.split('.').map(Number);
|
||||||
|
const partsB = coreB.split('.').map(Number);
|
||||||
|
const len = Math.max(partsA.length, partsB.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const va = partsA[i] ?? 0;
|
||||||
|
const vb = partsB[i] ?? 0;
|
||||||
|
if (va < vb) return true;
|
||||||
|
if (va > vb) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core versions are equal — compare pre-release
|
||||||
|
// No pre-release > any pre-release (1.0.0 > 1.0.0-alpha)
|
||||||
|
if (preA !== null && preB === null) return true;
|
||||||
|
if (preA === null && preB !== null) return false;
|
||||||
|
if (preA === null && preB === null) return false;
|
||||||
|
|
||||||
|
// Both have pre-release — compare dot-separated identifiers
|
||||||
|
const idsA = preA!.split('.');
|
||||||
|
const idsB = preB!.split('.');
|
||||||
|
const preLen = Math.max(idsA.length, idsB.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < preLen; i++) {
|
||||||
|
const ia = idsA[i];
|
||||||
|
const ib = idsB[i];
|
||||||
|
// Fewer fields = lower precedence
|
||||||
|
if (ia === undefined && ib !== undefined) return true;
|
||||||
|
if (ia !== undefined && ib === undefined) return false;
|
||||||
|
const na = /^\d+$/.test(ia!) ? parseInt(ia!, 10) : null;
|
||||||
|
const nb = /^\d+$/.test(ib!) ? parseInt(ib!, 10) : null;
|
||||||
|
// Numeric vs string: numeric < string
|
||||||
|
if (na !== null && nb !== null) {
|
||||||
|
if (na < nb) return true;
|
||||||
|
if (na > nb) return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (na !== null && nb === null) return true;
|
||||||
|
if (na === null && nb !== null) return false;
|
||||||
|
// Both strings
|
||||||
|
if (ia! < ib!) return true;
|
||||||
|
if (ia! > ib!) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Cache ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function readCache(): UpdateCheckResult | null {
|
||||||
|
try {
|
||||||
|
if (!existsSync(CACHE_FILE)) return null;
|
||||||
|
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as UpdateCheckResult;
|
||||||
|
const age = Date.now() - new Date(raw.checkedAt).getTime();
|
||||||
|
if (age > CACHE_TTL_MS) return null;
|
||||||
|
return raw;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCache(result: UpdateCheckResult): void {
|
||||||
|
try {
|
||||||
|
mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2) + '\n', 'utf-8');
|
||||||
|
} catch {
|
||||||
|
// Best-effort — cache is not critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently installed version of @mosaic/cli.
|
||||||
|
* Returns empty string if not installed.
|
||||||
|
*/
|
||||||
|
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 }>;
|
||||||
|
};
|
||||||
|
return data?.dependencies?.[CLI_PKG]?.version ?? '';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the latest published version from the Gitea npm registry.
|
||||||
|
* Returns empty string on failure.
|
||||||
|
*/
|
||||||
|
export function getLatestVersion(): string {
|
||||||
|
return npmExec(`view ${CLI_PKG} version --registry=${REGISTRY}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an update check — uses cache when fresh, otherwise hits registry.
|
||||||
|
* Never throws.
|
||||||
|
*/
|
||||||
|
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
|
||||||
|
if (!options?.skipCache) {
|
||||||
|
const cached = readCache();
|
||||||
|
if (cached) return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = getInstalledVersion();
|
||||||
|
const latest = getLatestVersion();
|
||||||
|
const updateAvailable = !!(current && latest && semverLt(current, latest));
|
||||||
|
|
||||||
|
const result: UpdateCheckResult = {
|
||||||
|
current,
|
||||||
|
latest,
|
||||||
|
updateAvailable,
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
registry: REGISTRY,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeCache(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a human-readable update notice. Returns empty string if up-to-date.
|
||||||
|
*/
|
||||||
|
export function formatUpdateNotice(result: UpdateCheckResult): string {
|
||||||
|
if (!result.updateAvailable) return '';
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'',
|
||||||
|
'╭─────────────────────────────────────────────────╮',
|
||||||
|
'│ │',
|
||||||
|
`│ Update available: ${result.current} → ${result.latest}`.padEnd(50) + '│',
|
||||||
|
'│ │',
|
||||||
|
'│ Run: bash tools/install.sh │',
|
||||||
|
'│ Or: npm i -g @mosaic/cli@latest │',
|
||||||
|
'│ │',
|
||||||
|
'╰─────────────────────────────────────────────────╯',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-blocking update check that prints a notice to stderr if an update
|
||||||
|
* is available. Designed to be called at CLI startup without delaying
|
||||||
|
* the user.
|
||||||
|
*/
|
||||||
|
export function backgroundUpdateCheck(): void {
|
||||||
|
try {
|
||||||
|
const result = checkForUpdate();
|
||||||
|
const notice = formatUpdateNotice(result);
|
||||||
|
if (notice) {
|
||||||
|
process.stderr.write(notice);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently ignore — never block the user
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,18 @@ source "$SCRIPT_DIR/common.sh"
|
|||||||
ensure_repo_root
|
ensure_repo_root
|
||||||
load_repo_hooks
|
load_repo_hooks
|
||||||
|
|
||||||
|
# ─── Update check (non-blocking) ────────────────────────────────────────────
|
||||||
|
if command -v mosaic &>/dev/null; then
|
||||||
|
if mosaic update --check 2>/dev/null; then
|
||||||
|
: # up to date
|
||||||
|
elif [[ $? -eq 2 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "[agent-framework] ⚠ A newer version of Mosaic CLI is available."
|
||||||
|
echo "[agent-framework] Run: mosaic update or bash tools/install.sh"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && has_remote; then
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && has_remote; then
|
||||||
if git diff --quiet && git diff --cached --quiet; then
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
run_step "Pull latest changes" git pull --rebase
|
run_step "Pull latest changes" git pull --rebase
|
||||||
|
|||||||
343
tools/install.sh
Executable file
343
tools/install.sh
Executable file
@@ -0,0 +1,343 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ─── Mosaic Stack Installer / Upgrader ────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Installs both components:
|
||||||
|
# 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools)
|
||||||
|
# 2. @mosaic/cli (npm) → ~/.npm-global/ (TUI, gateway client, wizard)
|
||||||
|
#
|
||||||
|
# Remote install (recommended):
|
||||||
|
# bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
|
||||||
|
#
|
||||||
|
# Remote install (alternative — use -s -- to pass flags):
|
||||||
|
# curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh | bash -s --
|
||||||
|
#
|
||||||
|
# Flags:
|
||||||
|
# --check Version check only, no install
|
||||||
|
# --framework Install/upgrade framework only (skip npm CLI)
|
||||||
|
# --cli Install/upgrade npm CLI only (skip framework)
|
||||||
|
# --ref <branch> Git ref for framework archive (default: main)
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
||||||
|
# MOSAIC_REGISTRY — npm registry URL (default: Gitea instance)
|
||||||
|
# MOSAIC_SCOPE — npm scope (default: @mosaic)
|
||||||
|
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
|
||||||
|
# MOSAIC_NO_COLOR — disable colour (set to 1)
|
||||||
|
# MOSAIC_REF — git ref for framework (default: main)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Wrapped in main() for safe curl-pipe usage.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
main() {
|
||||||
|
|
||||||
|
# ─── parse flags ──────────────────────────────────────────────────────────────
|
||||||
|
FLAG_CHECK=false
|
||||||
|
FLAG_FRAMEWORK=true
|
||||||
|
FLAG_CLI=true
|
||||||
|
GIT_REF="${MOSAIC_REF:-main}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--check) FLAG_CHECK=true; shift ;;
|
||||||
|
--framework) FLAG_CLI=false; shift ;;
|
||||||
|
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||||||
|
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── constants ────────────────────────────────────────────────────────────────
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaic/npm/}"
|
||||||
|
SCOPE="${MOSAIC_SCOPE:-@mosaic}"
|
||||||
|
PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}"
|
||||||
|
CLI_PKG="${SCOPE}/cli"
|
||||||
|
REPO_BASE="https://git.mosaicstack.dev/mosaic/mosaic-stack"
|
||||||
|
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
|
||||||
|
|
||||||
|
# ─── colours ──────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then
|
||||||
|
R="" G="" Y="" B="" C="" DIM="" BOLD="" RESET=""
|
||||||
|
else
|
||||||
|
R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m'
|
||||||
|
B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m'
|
||||||
|
BOLD=$'\033[1m' RESET=$'\033[0m'
|
||||||
|
fi
|
||||||
|
|
||||||
|
info() { echo "${B}ℹ${RESET} $*"; }
|
||||||
|
ok() { echo "${G}✔${RESET} $*"; }
|
||||||
|
warn() { echo "${Y}⚠${RESET} $*"; }
|
||||||
|
fail() { echo "${R}✖${RESET} $*" >&2; }
|
||||||
|
dim() { echo "${DIM}$*${RESET}"; }
|
||||||
|
step() { echo ""; echo "${BOLD}$*${RESET}"; }
|
||||||
|
|
||||||
|
# ─── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
if ! command -v "$1" &>/dev/null; then
|
||||||
|
fail "Required command not found: $1"
|
||||||
|
echo " Install it and re-run this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
installed_cli_version() {
|
||||||
|
local json
|
||||||
|
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
|
||||||
|
if [[ -n "$json" ]]; then
|
||||||
|
node -e "
|
||||||
|
const d = JSON.parse(process.argv[1]);
|
||||||
|
const v = d?.dependencies?.['${CLI_PKG}']?.version ?? '';
|
||||||
|
process.stdout.write(v);
|
||||||
|
" "$json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_cli_version() {
|
||||||
|
npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
version_lt() {
|
||||||
|
node -e "
|
||||||
|
const a=process.argv[1], b=process.argv[2];
|
||||||
|
const sp = v => { const i=v.indexOf('-'); return i===-1 ? [v,null] : [v.slice(0,i),v.slice(i+1)]; };
|
||||||
|
const [cA,pA]=sp(a.replace(/^v/,'')), [cB,pB]=sp(b.replace(/^v/,''));
|
||||||
|
const nA=cA.split('.').map(Number), nB=cB.split('.').map(Number);
|
||||||
|
for(let i=0;i<3;i++){if((nA[i]||0)<(nB[i]||0))process.exit(0);if((nA[i]||0)>(nB[i]||0))process.exit(1);}
|
||||||
|
if(pA!==null&&pB===null)process.exit(0);
|
||||||
|
if(pA===null)process.exit(1);
|
||||||
|
if(pA<pB)process.exit(0);
|
||||||
|
process.exit(1);
|
||||||
|
" "$1" "$2" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
framework_version() {
|
||||||
|
# Read VERSION from the installed mosaic launcher
|
||||||
|
local mosaic_bin="$MOSAIC_HOME/bin/mosaic"
|
||||||
|
if [[ -f "$mosaic_bin" ]]; then
|
||||||
|
grep -m1 '^VERSION=' "$mosaic_bin" 2>/dev/null | cut -d'"' -f2 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_cmd node
|
||||||
|
require_cmd npm
|
||||||
|
|
||||||
|
NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))')"
|
||||||
|
if [[ "$NODE_MAJOR" -lt 20 ]]; then
|
||||||
|
fail "Node.js >= 20 required (found v$(node --version))"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "${BOLD}Mosaic Stack Installer${RESET}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# PART 1: Framework (bash launcher + guides + runtime configs + tools)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
|
||||||
|
step "Framework (~/.config/mosaic)"
|
||||||
|
|
||||||
|
FRAMEWORK_CURRENT="$(framework_version)"
|
||||||
|
HAS_FRAMEWORK=false
|
||||||
|
[[ -d "$MOSAIC_HOME/bin" ]] && [[ -f "$MOSAIC_HOME/bin/mosaic" ]] && HAS_FRAMEWORK=true
|
||||||
|
|
||||||
|
if [[ -n "$FRAMEWORK_CURRENT" ]]; then
|
||||||
|
dim " Installed: framework v${FRAMEWORK_CURRENT}"
|
||||||
|
elif [[ "$HAS_FRAMEWORK" == "true" ]]; then
|
||||||
|
dim " Installed: framework (version unknown)"
|
||||||
|
else
|
||||||
|
dim " Installed: (none)"
|
||||||
|
fi
|
||||||
|
dim " Source: ${REPO_BASE} (ref: ${GIT_REF})"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$FLAG_CHECK" == "true" ]]; then
|
||||||
|
if [[ "$HAS_FRAMEWORK" == "true" ]]; then
|
||||||
|
ok "Framework is installed."
|
||||||
|
else
|
||||||
|
warn "Framework not installed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Download repo archive and extract framework
|
||||||
|
require_cmd tar
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-XXXXXX")"
|
||||||
|
cleanup_work() { rm -rf "$WORK_DIR"; }
|
||||||
|
trap cleanup_work EXIT
|
||||||
|
|
||||||
|
info "Downloading framework from ${GIT_REF}…"
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
curl -fsSL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
||||||
|
elif command -v wget &>/dev/null; then
|
||||||
|
wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
||||||
|
else
|
||||||
|
fail "curl or wget required to download framework."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gitea archives extract to <repo-name>/ inside the work dir
|
||||||
|
EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
||||||
|
FRAMEWORK_SRC="$EXTRACTED_DIR/packages/mosaic/framework"
|
||||||
|
|
||||||
|
if [[ ! -d "$FRAMEWORK_SRC" ]]; then
|
||||||
|
fail "Framework not found in archive at packages/mosaic/framework/"
|
||||||
|
fail "Archive contents:"
|
||||||
|
ls -la "$WORK_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.)
|
||||||
|
info "Installing framework to ${MOSAIC_HOME}…"
|
||||||
|
MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
|
||||||
|
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
|
||||||
|
MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
|
||||||
|
bash "$FRAMEWORK_SRC/install.sh"
|
||||||
|
|
||||||
|
ok "Framework installed"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ensure framework bin is on PATH
|
||||||
|
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
|
||||||
|
if [[ ":$PATH:" != *":$FRAMEWORK_BIN:"* ]]; then
|
||||||
|
warn "$FRAMEWORK_BIN is not on your PATH"
|
||||||
|
dim " The 'mosaic' launcher lives here. Add to your shell rc:"
|
||||||
|
dim " export PATH=\"$FRAMEWORK_BIN:\$PATH\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# PART 2: @mosaic/cli (npm — TUI, gateway client, wizard)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if [[ "$FLAG_CLI" == "true" ]]; then
|
||||||
|
step "@mosaic/cli (npm package)"
|
||||||
|
|
||||||
|
# Ensure prefix dir
|
||||||
|
if [[ ! -d "$PREFIX" ]]; then
|
||||||
|
info "Creating global prefix directory: $PREFIX"
|
||||||
|
mkdir -p "$PREFIX"/{bin,lib}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure npmrc scope mapping
|
||||||
|
NPMRC="$HOME/.npmrc"
|
||||||
|
SCOPE_LINE="${SCOPE}:registry=${REGISTRY}"
|
||||||
|
|
||||||
|
if ! grep -qF "$SCOPE_LINE" "$NPMRC" 2>/dev/null; then
|
||||||
|
info "Adding ${SCOPE} registry to $NPMRC"
|
||||||
|
echo "$SCOPE_LINE" >> "$NPMRC"
|
||||||
|
ok "Registry configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -qF "prefix=$PREFIX" "$NPMRC" 2>/dev/null; then
|
||||||
|
if ! grep -q '^prefix=' "$NPMRC" 2>/dev/null; then
|
||||||
|
echo "prefix=$PREFIX" >> "$NPMRC"
|
||||||
|
info "Set npm global prefix to $PREFIX"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT="$(installed_cli_version)"
|
||||||
|
LATEST="$(latest_cli_version)"
|
||||||
|
|
||||||
|
if [[ -n "$CURRENT" ]]; then
|
||||||
|
dim " Installed: ${CLI_PKG}@${CURRENT}"
|
||||||
|
else
|
||||||
|
dim " Installed: (none)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$LATEST" ]]; then
|
||||||
|
dim " Latest: ${CLI_PKG}@${LATEST}"
|
||||||
|
else
|
||||||
|
dim " Latest: (registry unreachable)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$FLAG_CHECK" == "true" ]]; then
|
||||||
|
if [[ -z "$LATEST" ]]; then
|
||||||
|
warn "Could not reach registry."
|
||||||
|
elif [[ -z "$CURRENT" ]]; then
|
||||||
|
warn "Not installed."
|
||||||
|
elif [[ "$CURRENT" == "$LATEST" ]]; then
|
||||||
|
ok "Up to date."
|
||||||
|
elif version_lt "$CURRENT" "$LATEST"; then
|
||||||
|
warn "Update available: $CURRENT → $LATEST"
|
||||||
|
else
|
||||||
|
ok "Up to date (or ahead of registry)."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -z "$LATEST" ]]; then
|
||||||
|
warn "Could not reach registry at $REGISTRY — skipping npm CLI."
|
||||||
|
elif [[ -z "$CURRENT" ]]; then
|
||||||
|
info "Installing ${CLI_PKG}@${LATEST}…"
|
||||||
|
npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
|
||||||
|
ok "CLI installed: $(installed_cli_version)"
|
||||||
|
elif [[ "$CURRENT" == "$LATEST" ]]; then
|
||||||
|
ok "Already at latest version ($LATEST)."
|
||||||
|
elif version_lt "$CURRENT" "$LATEST"; then
|
||||||
|
info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…"
|
||||||
|
npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
|
||||||
|
ok "CLI upgraded: $(installed_cli_version)"
|
||||||
|
else
|
||||||
|
ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PATH check for npm prefix
|
||||||
|
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||||
|
warn "$PREFIX/bin is not on your PATH"
|
||||||
|
dim " The 'mosaic' TUI/gateway CLI lives here (separate from the launcher)."
|
||||||
|
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Summary
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if [[ "$FLAG_CHECK" == "false" ]]; then
|
||||||
|
step "Summary"
|
||||||
|
|
||||||
|
echo " ${BOLD}Framework launcher:${RESET} $MOSAIC_HOME/bin/mosaic"
|
||||||
|
echo " ${DIM}mosaic claude, mosaic yolo claude, mosaic pi, mosaic doctor, …${RESET}"
|
||||||
|
echo ""
|
||||||
|
echo " ${BOLD}npm CLI (TUI):${RESET} $PREFIX/bin/mosaic"
|
||||||
|
echo " ${DIM}mosaic tui, mosaic login, mosaic wizard, mosaic update, …${RESET}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Warn if there's a naming collision (both on PATH)
|
||||||
|
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
|
||||||
|
if [[ ":$PATH:" == *":$FRAMEWORK_BIN:"* ]] && [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
|
||||||
|
# Check which one wins
|
||||||
|
WHICH_MOSAIC="$(command -v mosaic 2>/dev/null || true)"
|
||||||
|
if [[ -n "$WHICH_MOSAIC" ]]; then
|
||||||
|
dim " Active 'mosaic' binary: $WHICH_MOSAIC"
|
||||||
|
if [[ "$WHICH_MOSAIC" == "$FRAMEWORK_BIN/mosaic" ]]; then
|
||||||
|
dim " (Framework launcher takes priority — this is correct)"
|
||||||
|
else
|
||||||
|
warn "npm CLI shadows the framework launcher!"
|
||||||
|
dim " Ensure $FRAMEWORK_BIN appears BEFORE $PREFIX/bin in your PATH."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# First install guidance
|
||||||
|
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
||||||
|
echo ""
|
||||||
|
info "First install detected. Set up your agent identity:"
|
||||||
|
echo " ${C}mosaic init${RESET} (interactive SOUL.md / USER.md setup)"
|
||||||
|
echo " ${C}mosaic wizard${RESET} (full guided wizard via Node.js)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
ok "Done."
|
||||||
|
fi
|
||||||
|
|
||||||
|
} # end main
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user