diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index dafacdc..9fbe807 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -22,6 +22,94 @@ const program = new Command(); program.name('mosaic').description('Mosaic Stack CLI').version(CLI_VERSION); +// ─── framework launcher delegation ────────────────────────────────────── +// These commands delegate to the bash framework launcher (mosaic-launch) +// which handles runtime injection, mission context, session locks, etc. + +import { execFileSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; + +const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic'); +const LAUNCHER_PATH = join(MOSAIC_HOME, 'bin', 'mosaic-launch'); + +/** + * Delegate a command to the framework's mosaic-launch bash script. + * Passes all remaining args through and inherits stdio. + */ +function delegateToLauncher(command: string, args: string[]): never { + if (!existsSync(LAUNCHER_PATH)) { + console.error(`Framework launcher not found: ${LAUNCHER_PATH}`); + console.error( + 'Install the framework: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh) --framework', + ); + process.exit(1); + } + try { + execFileSync(LAUNCHER_PATH, [command, ...args], { + stdio: 'inherit', + env: { ...process.env, MOSAIC_HOME }, + }); + process.exit(0); + } catch (err) { + const code = (err as { status?: number }).status ?? 1; + process.exit(code); + } +} + +// Runtime launchers +for (const runtime of ['claude', 'codex', 'opencode', 'pi'] as const) { + program + .command(runtime) + .description( + `Launch ${ + runtime === 'pi' + ? 'Pi' + : runtime === 'claude' + ? 'Claude Code' + : runtime.charAt(0).toUpperCase() + runtime.slice(1) + } with Mosaic injection`, + ) + .allowUnknownOption(true) + .allowExcessArguments(true) + .action((_opts: unknown, cmd: Command) => { + delegateToLauncher(runtime, cmd.args); + }); +} + +// Yolo mode +program + .command('yolo ') + .description('Launch a runtime in dangerous-permissions mode') + .allowUnknownOption(true) + .allowExcessArguments(true) + .action((runtime: string, _opts: unknown, cmd: Command) => { + delegateToLauncher('yolo', [runtime, ...cmd.args]); + }); + +// Framework management commands that delegate to mosaic-launch +const DELEGATED_COMMANDS: Record = { + init: 'Generate SOUL.md (agent identity contract)', + doctor: 'Health audit — detect drift and missing files', + sync: 'Sync skills from canonical source', + seq: 'sequential-thinking MCP management (check/fix/start)', + bootstrap: 'Bootstrap a repo with Mosaic standards', + coord: 'Manual mission coordinator tools', + upgrade: 'Upgrade installed Mosaic release', +}; + +for (const [name, desc] of Object.entries(DELEGATED_COMMANDS)) { + program + .command(name, { hidden: false }) + .description(desc) + .allowUnknownOption(true) + .allowExcessArguments(true) + .action((_opts: unknown, cmd: Command) => { + delegateToLauncher(name, cmd.args); + }); +} + // ─── login ────────────────────────────────────────────────────────────── program diff --git a/packages/mosaic/framework/bin/mosaic b/packages/mosaic/framework/bin/mosaic-launch similarity index 97% rename from packages/mosaic/framework/bin/mosaic rename to packages/mosaic/framework/bin/mosaic-launch index 5a686f9..b696a40 100755 --- a/packages/mosaic/framework/bin/mosaic +++ b/packages/mosaic/framework/bin/mosaic-launch @@ -1,25 +1,21 @@ #!/usr/bin/env bash set -euo pipefail -# mosaic — Unified agent launcher and management CLI +# mosaic-launch — Framework agent launcher (called by the mosaic npm CLI) # # AGENTS.md is the global policy source for all agent sessions. # The launcher injects a composed runtime contract (AGENTS + runtime reference). # -# Usage: +# Usage (via mosaic CLI): # mosaic claude [args...] Launch Claude Code with runtime contract injected -# mosaic opencode [args...] Launch OpenCode with runtime contract injected # mosaic codex [args...] Launch Codex with runtime contract injected +# mosaic opencode [args...] Launch OpenCode with runtime contract injected +# mosaic pi [args...] Launch Pi with runtime contract injected # mosaic yolo [args...] Launch runtime in dangerous-permissions mode -# mosaic --yolo [args...] Alias for yolo -# mosaic init [args...] Generate SOUL.md interactively -# mosaic doctor [args...] Health audit -# mosaic sync [args...] Sync skills -# mosaic seq [subcommand] sequential-thinking MCP management (check/fix/start) -# mosaic bootstrap Bootstrap a repo -# mosaic upgrade release Upgrade installed Mosaic release -# mosaic upgrade check Check release upgrade status (no changes) -# mosaic upgrade project [args] Upgrade project-local stale files +# +# Direct usage: +# mosaic-launch claude [args...] +# mosaic-launch yolo claude [args...] MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" VERSION="0.1.0" diff --git a/packages/mosaic/framework/install.sh b/packages/mosaic/framework/install.sh index aae05be..d79eca6 100755 --- a/packages/mosaic/framework/install.sh +++ b/packages/mosaic/framework/install.sh @@ -4,7 +4,7 @@ set -euo pipefail SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}" INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}" # prompt|keep|overwrite -PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory") +PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory" "sources") # Colors (disabled if not a terminal) if [[ -t 1 ]]; then @@ -21,7 +21,7 @@ step() { echo -e "\n${BOLD}$1${RESET}"; } is_existing_install() { [[ -d "$TARGET_DIR" ]] || return 1 - [[ -f "$TARGET_DIR/bin/mosaic" || -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]] + [[ -f "$TARGET_DIR/bin/mosaic-launch" || -f "$TARGET_DIR/bin/mosaic" || -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]] } select_install_mode() { diff --git a/packages/mosaic/framework/tools/orchestrator/session-run.sh b/packages/mosaic/framework/tools/orchestrator/session-run.sh index dcbda36..4196ca2 100755 --- a/packages/mosaic/framework/tools/orchestrator/session-run.sh +++ b/packages/mosaic/framework/tools/orchestrator/session-run.sh @@ -80,11 +80,11 @@ echo -e "${C_CYAN}Capsule:${C_RESET} $(next_task_capsule_path "$PROJECT")" cd "$PROJECT" if [[ "$YOLO" == true ]]; then - exec "$MOSAIC_HOME/bin/mosaic" yolo "$runtime" "$launch_prompt" + exec "$MOSAIC_HOME/bin/mosaic-launch" yolo "$runtime" "$launch_prompt" elif [[ "$runtime" == "claude" ]]; then - exec "$MOSAIC_HOME/bin/mosaic" claude "$launch_prompt" + exec "$MOSAIC_HOME/bin/mosaic-launch" claude "$launch_prompt" elif [[ "$runtime" == "codex" ]]; then - exec "$MOSAIC_HOME/bin/mosaic" codex "$launch_prompt" + exec "$MOSAIC_HOME/bin/mosaic-launch" codex "$launch_prompt" fi echo -e "${C_RED}Unsupported coord runtime: $runtime${C_RESET}" >&2 diff --git a/tools/install.sh b/tools/install.sh index 4a4cffd..0cb6a9e 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -202,13 +202,8 @@ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then 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 + # Framework bin is no longer needed on PATH — the npm CLI delegates + # to mosaic-launch directly via its absolute path. fi fi @@ -290,7 +285,6 @@ if [[ "$FLAG_CLI" == "true" ]]; then # 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 @@ -303,26 +297,19 @@ fi 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 " ${BOLD}mosaic CLI:${RESET} $PREFIX/bin/mosaic" + dim " All commands: mosaic claude, mosaic yolo pi, mosaic tui, mosaic doctor, …" echo "" + dim " Framework data: $MOSAIC_HOME/" + dim " Launcher backend: $MOSAIC_HOME/bin/mosaic-launch" - # Warn if there's a naming collision (both on PATH) + # Clean up old mosaic binary from PATH if framework bin is still there 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 + if [[ ":$PATH:" == *":$FRAMEWORK_BIN:"* ]]; then + OLD_MOSAIC="$FRAMEWORK_BIN/mosaic" + if [[ -f "$OLD_MOSAIC" ]] && [[ ! -L "$OLD_MOSAIC" ]]; then + info "Removing old framework 'mosaic' binary (replaced by npm CLI)" + rm -f "$OLD_MOSAIC" fi fi