diff --git a/eslint.config.mjs b/eslint.config.mjs index 1a2c789..f105948 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -27,6 +27,7 @@ export default tseslint.config( 'apps/web/e2e/*.ts', 'apps/web/e2e/helpers/*.ts', 'apps/web/playwright.config.ts', + 'packages/mosaic/__tests__/*.ts', ], }, }, diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f54c846..dafacdc 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -10,6 +10,14 @@ import { registerPrdyCommand } from './commands/prdy.js'; const _require = createRequire(import.meta.url); 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(); program.name('mosaic').description('Mosaic Stack CLI').version(CLI_VERSION); @@ -297,6 +305,54 @@ if (qrCmd !== undefined) { 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 ───────────────────────────────────────────────────────────── program diff --git a/packages/mosaic/__tests__/integration/full-wizard.test.ts b/packages/mosaic/__tests__/integration/full-wizard.test.ts index c46242b..b045f7f 100644 --- a/packages/mosaic/__tests__/integration/full-wizard.test.ts +++ b/packages/mosaic/__tests__/integration/full-wizard.test.ts @@ -20,10 +20,13 @@ describe('Full Wizard (headless)', () => { beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-wizard-test-')); - // Copy templates to tmp dir - const templatesDir = join(repoRoot, 'templates'); - if (existsSync(templatesDir)) { - cpSync(templatesDir, join(tmpDir, 'templates'), { recursive: true }); + // Copy templates to tmp dir — templates live under framework/ after monorepo migration + const candidates = [join(repoRoot, 'framework', 'templates'), join(repoRoot, 'templates')]; + for (const templatesDir of candidates) { + if (existsSync(templatesDir)) { + cpSync(templatesDir, join(tmpDir, 'templates'), { recursive: true }); + break; + } } }); diff --git a/packages/mosaic/__tests__/update-checker.test.ts b/packages/mosaic/__tests__/update-checker.test.ts new file mode 100644 index 0000000..1bf25e2 --- /dev/null +++ b/packages/mosaic/__tests__/update-checker.test.ts @@ -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'); + }); +}); diff --git a/packages/mosaic/framework/bin/mosaic b/packages/mosaic/framework/bin/mosaic index 2b9b352..5a686f9 100755 --- a/packages/mosaic/framework/bin/mosaic +++ b/packages/mosaic/framework/bin/mosaic @@ -80,7 +80,7 @@ USAGE check_mosaic_home() { if [[ ! -d "$MOSAIC_HOME" ]]; then 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 fi } @@ -88,7 +88,7 @@ check_mosaic_home() { check_agents_md() { if [[ ! -f "$MOSAIC_HOME/AGENTS.md" ]]; then echo "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." >&2 - echo "[mosaic] Re-run the installer: npm install -g @mosaic/mosaic" >&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 fi } diff --git a/packages/mosaic/framework/bin/mosaic-release-upgrade b/packages/mosaic/framework/bin/mosaic-release-upgrade index 282650b..2bbbd09 100755 --- a/packages/mosaic/framework/bin/mosaic-release-upgrade +++ b/packages/mosaic/framework/bin/mosaic-release-upgrade @@ -12,7 +12,7 @@ set -euo pipefail # mosaic-release-upgrade --ref v0.2.0 --overwrite --yes 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}" INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" # keep|overwrite YES=false diff --git a/packages/mosaic/framework/bin/mosaic-release-upgrade.ps1 b/packages/mosaic/framework/bin/mosaic-release-upgrade.ps1 index 016ed7a..dc76cb3 100644 --- a/packages/mosaic/framework/bin/mosaic-release-upgrade.ps1 +++ b/packages/mosaic/framework/bin/mosaic-release-upgrade.ps1 @@ -19,7 +19,7 @@ $MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:U $RemoteInstallerUrl = if ($env:MOSAIC_REMOTE_INSTALL_URL) { $env:MOSAIC_REMOTE_INSTALL_URL } 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" } diff --git a/packages/mosaic/framework/bin/mosaic.ps1 b/packages/mosaic/framework/bin/mosaic.ps1 index fcc0bfb..81b7e34 100644 --- a/packages/mosaic/framework/bin/mosaic.ps1 +++ b/packages/mosaic/framework/bin/mosaic.ps1 @@ -49,7 +49,7 @@ Options: function Assert-MosaicHome { if (-not (Test-Path $MosaicHome)) { 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 } } diff --git a/packages/mosaic/framework/defaults/README.md b/packages/mosaic/framework/defaults/README.md index 0dc41b9..3ffeaa3 100644 --- a/packages/mosaic/framework/defaults/README.md +++ b/packages/mosaic/framework/defaults/README.md @@ -1,43 +1,41 @@ # 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. -> **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 ### Mac / Linux ```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) ```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) ```bash -git clone https://git.mosaicstack.dev/mosaic/bootstrap.git ~/src/mosaic-bootstrap -cd ~/src/mosaic-bootstrap && bash install.sh +git clone git@git.mosaicstack.dev:mosaic/mosaic-stack.git ~/src/mosaic-stack +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: - -- Install the framework to `~/.config/mosaic/` -- Add `~/.config/mosaic/bin` to your PATH -- Sync runtime adapters and skills -- Install and configure sequential-thinking MCP (hard requirement) -- Run a health audit -- Detect existing installs and prompt to keep or overwrite local files -- Prompt you to run `mosaic init` to set up your agent identity +- Downloads the framework from the monorepo archive +- Installs it to `~/.config/mosaic/` +- Installs `@mosaic/cli` globally via npm (TUI, gateway client, wizard) +- Adds `~/.config/mosaic/bin` to your PATH +- Syncs runtime adapters and skills +- Runs a health audit +- Detects existing installs and preserves local files (SOUL.md, USER.md, etc.) ## 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) - `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 @@ -77,9 +75,12 @@ If Node.js is unavailable, `mosaic init` falls back to the bash-based `mosaic-in ## Launching Agent Sessions ```bash +mosaic pi # Launch Pi with full Mosaic injection (recommended) mosaic claude # Launch Claude Code with full Mosaic injection mosaic codex # Launch Codex 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: @@ -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) ├── TOOLS.md ← Machine-level tool reference (generated by mosaic init) ├── STANDARDS.md ← Machine-wide standards -├── guides/E2E-DELIVERY.md ← Mandatory E2E software delivery procedure -├── guides/PRD.md ← Mandatory PRD requirements gate before coding -├── guides/DOCUMENTATION.md ← Mandatory documentation standard and gates -├── 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. +├── guides/ ← Operational guides (E2E delivery, PRD, docs, etc.) +├── bin/ ← CLI tools (mosaic launcher, mosaic-init, mosaic-doctor, etc.) +├── tools/ ← Tool suites: git, orchestrator, prdy, quality, etc. ├── runtime/ ← Runtime adapters + runtime-specific references -│ ├── claude/CLAUDE.md -│ ├── claude/RUNTIME.md -│ ├── opencode/AGENTS.md -│ ├── opencode/RUNTIME.md -│ ├── codex/instructions.md -│ ├── codex/RUNTIME.md -│ └── mcp/SEQUENTIAL-THINKING.json +│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks +│ ├── codex/ ← instructions.md, RUNTIME.md +│ ├── opencode/ ← AGENTS.md, RUNTIME.md +│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts +│ └── mcp/ ← MCP server configs ├── skills/ ← Universal skills (synced from mosaic/agent-skills) ├── skills-local/ ← Local cross-runtime skills +├── memory/ ← Persistent agent memory (preserved across upgrades) └── 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 | | ------------------- | ----------------------------------------------------------------------------------------- | +| `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 codex` | Writes composed runtime contract to `~/.codex/instructions.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 | | `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 ```bash @@ -142,126 +136,53 @@ mosaic init # Interactive wizard (or legacy init) mosaic doctor # Health audit — detect drift and missing files mosaic sync # Sync skills from canonical source mosaic bootstrap # Bootstrap a repo with Mosaic standards -mosaic upgrade check # Check release upgrade status (no changes) -mosaic upgrade # Upgrade installed Mosaic release (keeps SOUL.md by default) -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) +mosaic upgrade # Upgrade installed Mosaic release +mosaic upgrade check # Check upgrade status (no changes) ``` -## Upgrading Mosaic Release +## Upgrading -Upgrade the installed framework in place: +Run the installer again — it handles upgrades automatically: ```bash -# Default (safe): keep local SOUL.md, USER.md, TOOLS.md + memory -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 +bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh) ``` -`mosaic upgrade` re-runs the remote installer and passes install mode controls (`keep`/`overwrite`). -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: +Or from a local checkout: ```bash -# Preview what would change across all projects -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 +cd ~/src/mosaic-stack && git pull && bash tools/install.sh ``` -Backward compatibility is preserved for historical usage: +The installer preserves local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/` by default. + +### Flags ```bash -mosaic upgrade --all # still routes to project-upgrade -mosaic upgrade ~/src/my-repo # still routes to project-upgrade +bash tools/install.sh --check # Version check only +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 -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 mosaic sync # Full sync (clone + link) ~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only ``` -## Runtime Compatibility - -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: +## Health Audit ```bash -~/.config/mosaic/bin/mosaic-link-runtime-assets +mosaic doctor # Standard audit +~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode ``` ## 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 -- npx -y - -# Register an HTTP MCP (e.g. OpenBrain) -claude mcp add --scope user --transport http \ - --header "Authorization: Bearer " - -# 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 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 ``` -### 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 -claude mcp add --scope user --transport http openbrain https://your-openbrain-host/mcp \ - --header "Authorization: Bearer YOUR_TOKEN" -``` - -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 +claude mcp add --scope user -- npx -y +claude mcp add --scope user --transport http --header "Authorization: Bearer " +claude mcp list ``` diff --git a/packages/mosaic/framework/remote-install.ps1 b/packages/mosaic/framework/remote-install.ps1 deleted file mode 100644 index 635ab11..0000000 --- a/packages/mosaic/framework/remote-install.ps1 +++ /dev/null @@ -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 -} diff --git a/packages/mosaic/framework/remote-install.sh b/packages/mosaic/framework/remote-install.sh deleted file mode 100755 index 114be02..0000000 --- a/packages/mosaic/framework/remote-install.sh +++ /dev/null @@ -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" 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; + }; + 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 + } +} diff --git a/scripts/agent/session-start.sh b/scripts/agent/session-start.sh index 613eaad..4753558 100755 --- a/scripts/agent/session-start.sh +++ b/scripts/agent/session-start.sh @@ -8,6 +8,18 @@ source "$SCRIPT_DIR/common.sh" ensure_repo_root 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 diff --quiet && git diff --cached --quiet; then run_step "Pull latest changes" git pull --rebase diff --git a/tools/install.sh b/tools/install.sh new file mode 100755 index 0000000..4a4cffd --- /dev/null +++ b/tools/install.sh @@ -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 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/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 / 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 "$@"