From 45f5b9062e8dba91216359e1d14a6479566981f5 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 1 Apr 2026 21:03:23 -0500 Subject: [PATCH 1/4] feat: install.sh + auto-update checker for CLI - tools/install.sh: standalone installer/upgrader, curl-pipe safe (main() wrapper, process.argv instead of stdin, mkdir -p prefix) - packages/mosaic/src/runtime/update-checker.ts: version check module with 1h cache at ~/.cache/mosaic/update-check.json - CLI startup: non-blocking background update check on every invocation - 'mosaic update' command: explicit check + install (--check for CI) - session-start.sh: warns agents when CLI is outdated - Proper semver comparison including pre-release precedence - eslint: allow __tests__ in packages/mosaic for projectService --- eslint.config.mjs | 1 + packages/cli/src/cli.ts | 54 ++++ .../mosaic/__tests__/update-checker.test.ts | 52 ++++ packages/mosaic/src/index.ts | 9 + packages/mosaic/src/runtime/update-checker.ts | 238 ++++++++++++++++++ scripts/agent/session-start.sh | 12 + tools/install.sh | 218 ++++++++++++++++ 7 files changed, 584 insertions(+) create mode 100644 packages/mosaic/__tests__/update-checker.test.ts create mode 100644 packages/mosaic/src/runtime/update-checker.ts create mode 100755 tools/install.sh 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..21f6b94 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,52 @@ 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 { + execSync( + 'npm install -g @mosaic/cli@latest --registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/', + { 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__/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/src/index.ts b/packages/mosaic/src/index.ts index 2a82f62..f228583 100644 --- a/packages/mosaic/src/index.ts +++ b/packages/mosaic/src/index.ts @@ -13,6 +13,15 @@ import type { CommunicationStyle } from './types.js'; export { VERSION, DEFAULT_MOSAIC_HOME }; 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 { HeadlessPrompter } from './prompter/headless-prompter.js'; export { createConfigService } from './config/config-service.js'; diff --git a/packages/mosaic/src/runtime/update-checker.ts b/packages/mosaic/src/runtime/update-checker.ts new file mode 100644 index 0000000..0d9878e --- /dev/null +++ b/packages/mosaic/src/runtime/update-checker.ts @@ -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; + }; + 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..2fd8d2e --- /dev/null +++ b/tools/install.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# ─── Mosaic Stack Installer / Upgrader ──────────────────────────────────────── +# +# 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 -- +# curl -fsSL ... | bash -s -- --check +# +# Local (from repo checkout): +# bash tools/install.sh +# bash tools/install.sh --check # version check only +# +# Environment: +# 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) +# ────────────────────────────────────────────────────────────────────────────── +# +# The entire script is wrapped in main() so that bash reads it fully into +# memory before execution — required for safe curl-pipe usage. + +set -euo pipefail + +main() { + +# ─── constants ──────────────────────────────────────────────────────────────── +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" + +# ─── colours ────────────────────────────────────────────────────────────────── +# Detect colour support: works in terminals and bash <(curl …), but correctly +# disables when piped through a non-tty (curl … | bash). +if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then + R="" G="" Y="" B="" C="" DIM="" 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' 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}"; } + +# ─── 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 +} + +# Get the installed version of @mosaic/cli (empty string if not installed) +installed_version() { + local json + json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true + if [[ -n "$json" ]]; then + # Feed json via heredoc, not stdin pipe — safe in curl-pipe context + 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 +} + +# Get the latest published version from the registry +latest_version() { + npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true +} + +# Compare two semver strings: returns 0 (true) if $1 < $2 +version_lt() { + # Semver-aware comparison via node (handles pre-release correctly) + 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 +} + +# ─── 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 + +# ─── ensure prefix directory exists ────────────────────────────────────────── + +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 + +# Ensure prefix is set (only if no prefix= line exists yet) +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 + +# Ensure PREFIX/bin is on PATH +if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then + warn "$PREFIX/bin is not on your PATH" + dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\"" +fi + +# ─── version check ─────────────────────────────────────────────────────────── + +info "Checking versions…" + +CURRENT="$(installed_version)" +LATEST="$(latest_version)" + +if [[ -z "$LATEST" ]]; then + fail "Could not reach registry at $REGISTRY" + fail "Check network connectivity and registry URL." + exit 1 +fi + +echo "" +if [[ -n "$CURRENT" ]]; then + dim " Installed: ${CLI_PKG}@${CURRENT}" +else + dim " Installed: (none)" +fi +dim " Latest: ${CLI_PKG}@${LATEST}" +echo "" + +# --check flag: just report, don't install +if [[ "${1:-}" == "--check" ]]; then + if [[ -z "$CURRENT" ]]; then + warn "Not installed. Run without --check to install." + exit 1 + elif [[ "$CURRENT" == "$LATEST" ]]; then + ok "Up to date." + exit 0 + elif version_lt "$CURRENT" "$LATEST"; then + warn "Update available: $CURRENT → $LATEST" + exit 2 + else + ok "Up to date (or ahead of registry)." + exit 0 + fi +fi + +# ─── install / upgrade ────────────────────────────────────────────────────── + +if [[ -z "$CURRENT" ]]; then + info "Installing ${CLI_PKG}@${LATEST}…" +elif [[ "$CURRENT" == "$LATEST" ]]; then + ok "Already at latest version ($LATEST). Nothing to do." + exit 0 +elif version_lt "$CURRENT" "$LATEST"; then + info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…" +else + ok "Installed version ($CURRENT) is at or ahead of registry ($LATEST)." + exit 0 +fi + +npm install -g "${CLI_PKG}@${LATEST}" \ + --registry="$REGISTRY" \ + --prefix="$PREFIX" \ + 2>&1 | sed 's/^/ /' + +echo "" +ok "Mosaic CLI installed: $(installed_version)" +dim " Binary: $PREFIX/bin/mosaic" + +# ─── post-install: run wizard if first install ─────────────────────────────── + +if [[ -z "$CURRENT" ]]; then + echo "" + info "First install detected." + if [[ -t 0 ]] && [[ -t 1 ]]; then + echo " Run ${C}mosaic wizard${RESET} to set up your configuration." + else + dim " Run 'mosaic wizard' to set up your configuration." + fi +fi + +echo "" +ok "Done." + +} # end main + +main "$@" -- 2.49.1 From 2f68237046977c0349d387b6ead793437fda15ac Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 1 Apr 2026 21:06:40 -0500 Subject: [PATCH 2/4] fix: remove --registry from npm install to avoid 404 on transitive deps The @mosaic scope registry is configured in ~/.npmrc. Passing --registry on the install command overrides the default registry for ALL packages, causing non-@mosaic deps like @clack/prompts to 404 against Gitea. --- packages/cli/src/cli.ts | 10 ++++++---- tools/install.sh | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 21f6b94..dafacdc 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -340,10 +340,12 @@ program console.log('Installing update…'); try { - execSync( - 'npm install -g @mosaic/cli@latest --registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/', - { stdio: 'inherit', timeout: 60_000 }, - ); + // 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'); diff --git a/tools/install.sh b/tools/install.sh index 2fd8d2e..15d6362 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -189,8 +189,10 @@ else exit 0 fi +# NOTE: Do NOT pass --registry here. The @mosaic scope is already mapped +# in ~/.npmrc. Passing --registry globally would redirect ALL deps (including +# @clack/prompts, commander, etc.) to the Gitea registry, causing 404s. npm install -g "${CLI_PKG}@${LATEST}" \ - --registry="$REGISTRY" \ --prefix="$PREFIX" \ 2>&1 | sed 's/^/ /' -- 2.49.1 From 8a83aed9b18cbf477d99650a7e4cdb1cf901669e Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 1 Apr 2026 21:32:19 -0500 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20unify=20install.sh=20=E2=80=94=20si?= =?UTF-8?q?ngle=20installer=20for=20framework=20+=20npm=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/install.sh now installs both components: 1. Framework (bash launcher, guides, runtime configs) → ~/.config/mosaic/ 2. @mosaic/cli (TUI, gateway client, wizard) → ~/.npm-global/ - Downloads framework from monorepo archive (no bootstrap repo dependency) - Supports --framework, --cli, --check, --ref flags - Delete remote-install.sh and remote-install.ps1 (redundant redirectors) - Update all stale mosaic/bootstrap references → mosaic/mosaic-stack - Update README.md with monorepo install instructions Deprecates: mosaic/bootstrap repo --- packages/mosaic/framework/bin/mosaic | 4 +- .../framework/bin/mosaic-release-upgrade | 2 +- .../framework/bin/mosaic-release-upgrade.ps1 | 2 +- packages/mosaic/framework/bin/mosaic.ps1 | 2 +- packages/mosaic/framework/defaults/README.md | 243 +++--------- packages/mosaic/framework/remote-install.ps1 | 38 -- packages/mosaic/framework/remote-install.sh | 63 --- tools/install.sh | 365 ++++++++++++------ 8 files changed, 300 insertions(+), 419 deletions(-) delete mode 100644 packages/mosaic/framework/remote-install.ps1 delete mode 100755 packages/mosaic/framework/remote-install.sh 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" Git ref for framework archive (default: main) # # Environment: -# 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_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) # ────────────────────────────────────────────────────────────────────────────── # -# The entire script is wrapped in main() so that bash reads it fully into -# memory before execution — required for safe curl-pipe usage. +# 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 ────────────────────────────────────────────────────────────────── -# Detect colour support: works in terminals and bash <(curl …), but correctly -# disables when piped through a non-tty (curl … | bash). if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then - R="" G="" Y="" B="" C="" DIM="" RESET="" + 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' RESET=$'\033[0m' + B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m' + BOLD=$'\033[1m' RESET=$'\033[0m' fi info() { echo "${B}ℹ${RESET} $*"; } @@ -47,6 +71,7 @@ ok() { echo "${G}✔${RESET} $*"; } warn() { echo "${Y}⚠${RESET} $*"; } fail() { echo "${R}✖${RESET} $*" >&2; } dim() { echo "${DIM}$*${RESET}"; } +step() { echo ""; echo "${BOLD}$*${RESET}"; } # ─── helpers ────────────────────────────────────────────────────────────────── @@ -58,12 +83,10 @@ require_cmd() { fi } -# Get the installed version of @mosaic/cli (empty string if not installed) -installed_version() { +installed_cli_version() { local json json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true if [[ -n "$json" ]]; then - # Feed json via heredoc, not stdin pipe — safe in curl-pipe context node -e " const d = JSON.parse(process.argv[1]); const v = d?.dependencies?.['${CLI_PKG}']?.version ?? ''; @@ -72,14 +95,11 @@ installed_version() { fi } -# Get the latest published version from the registry -latest_version() { +latest_cli_version() { npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true } -# Compare two semver strings: returns 0 (true) if $1 < $2 version_lt() { - # Semver-aware comparison via node (handles pre-release correctly) 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)]; }; @@ -93,6 +113,14 @@ version_lt() { " "$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 @@ -104,116 +132,211 @@ if [[ "$NODE_MAJOR" -lt 20 ]]; then exit 1 fi -# ─── ensure prefix directory exists ────────────────────────────────────────── - -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 - -# Ensure prefix is set (only if no prefix= line exists yet) -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 - -# Ensure PREFIX/bin is on PATH -if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then - warn "$PREFIX/bin is not on your PATH" - dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\"" -fi - -# ─── version check ─────────────────────────────────────────────────────────── - -info "Checking versions…" - -CURRENT="$(installed_version)" -LATEST="$(latest_version)" - -if [[ -z "$LATEST" ]]; then - fail "Could not reach registry at $REGISTRY" - fail "Check network connectivity and registry URL." - exit 1 -fi - echo "" -if [[ -n "$CURRENT" ]]; then - dim " Installed: ${CLI_PKG}@${CURRENT}" -else - dim " Installed: (none)" -fi -dim " Latest: ${CLI_PKG}@${LATEST}" +echo "${BOLD}Mosaic Stack Installer${RESET}" echo "" -# --check flag: just report, don't install -if [[ "${1:-}" == "--check" ]]; then - if [[ -z "$CURRENT" ]]; then - warn "Not installed. Run without --check to install." - exit 1 - elif [[ "$CURRENT" == "$LATEST" ]]; then - ok "Up to date." - exit 0 - elif version_lt "$CURRENT" "$LATEST"; then - warn "Update available: $CURRENT → $LATEST" - exit 2 +# ═══════════════════════════════════════════════════════════════════════════════ +# 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 - ok "Up to date (or ahead of registry)." - exit 0 + dim " Installed: (none)" fi -fi - -# ─── install / upgrade ────────────────────────────────────────────────────── - -if [[ -z "$CURRENT" ]]; then - info "Installing ${CLI_PKG}@${LATEST}…" -elif [[ "$CURRENT" == "$LATEST" ]]; then - ok "Already at latest version ($LATEST). Nothing to do." - exit 0 -elif version_lt "$CURRENT" "$LATEST"; then - info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…" -else - ok "Installed version ($CURRENT) is at or ahead of registry ($LATEST)." - exit 0 -fi - -# NOTE: Do NOT pass --registry here. The @mosaic scope is already mapped -# in ~/.npmrc. Passing --registry globally would redirect ALL deps (including -# @clack/prompts, commander, etc.) to the Gitea registry, causing 404s. -npm install -g "${CLI_PKG}@${LATEST}" \ - --prefix="$PREFIX" \ - 2>&1 | sed 's/^/ /' - -echo "" -ok "Mosaic CLI installed: $(installed_version)" -dim " Binary: $PREFIX/bin/mosaic" - -# ─── post-install: run wizard if first install ─────────────────────────────── - -if [[ -z "$CURRENT" ]]; then + dim " Source: ${REPO_BASE} (ref: ${GIT_REF})" echo "" - info "First install detected." - if [[ -t 0 ]] && [[ -t 1 ]]; then - echo " Run ${C}mosaic wizard${RESET} to set up your configuration." + + if [[ "$FLAG_CHECK" == "true" ]]; then + if [[ "$HAS_FRAMEWORK" == "true" ]]; then + ok "Framework is installed." + else + warn "Framework not installed." + fi else - dim " Run 'mosaic wizard' to set up your configuration." + # 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 -echo "" -ok "Done." +# ═══════════════════════════════════════════════════════════════════════════════ +# 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 -- 2.49.1 From 3b9104429bb482f75bdfe96143ab65064caaa4a1 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 1 Apr 2026 21:51:34 -0500 Subject: [PATCH 4/4] =?UTF-8?q?fix(mosaic):=20wizard=20integration=20test?= =?UTF-8?q?=20=E2=80=94=20templates=20path=20after=20monorepo=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Templates moved from packages/mosaic/templates/ to packages/mosaic/framework/templates/ in #345. The test's existsSync guard silently skipped the copy, causing writeSoul to early-return without writing SOUL.md. --- .../mosaic/__tests__/integration/full-wizard.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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; + } } }); -- 2.49.1