From 361fece02345fc8817faec4de06543d5aa602705 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 2 Apr 2026 20:20:59 -0500 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20make=20mosaic=20init=20idempotent=20?= =?UTF-8?q?=E2=80=94=20detect=20existing=20config=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mosaic-init bash script: detect existing SOUL.md/USER.md/TOOLS.md and prompt user to keep, import (re-use values as defaults), or overwrite. Non-interactive mode exits cleanly unless --force is passed. Overwrite creates timestamped backups before replacing files. - launch.ts checkSoul(): prefer 'mosaic wizard' over legacy bash script when SOUL.md is missing, with fallback to mosaic-init. - detect-install.ts: pre-populate wizard state with existing values when user chooses 'reconfigure', so they see current settings as defaults. - soul-setup.ts: show existing agent name and communication style as defaults during reconfiguration. - Added tests for reconfigure pre-population and reset non-population. --- packages/cli/src/commands/launch.ts | 16 ++- .../__tests__/stages/detect-install.test.ts | 32 +++++ .../framework/tools/_scripts/mosaic-init | 131 ++++++++++++++++++ packages/mosaic/src/stages/detect-install.ts | 4 +- packages/mosaic/src/stages/soul-setup.ts | 16 ++- 5 files changed, 194 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/launch.ts b/packages/cli/src/commands/launch.ts index 524e9be..4105009 100644 --- a/packages/cli/src/commands/launch.ts +++ b/packages/cli/src/commands/launch.ts @@ -54,12 +54,24 @@ function checkRuntime(cmd: string): void { function checkSoul(): void { const soulPath = join(MOSAIC_HOME, 'SOUL.md'); if (!existsSync(soulPath)) { - console.log('[mosaic] SOUL.md not found. Running mosaic init...'); + console.log('[mosaic] SOUL.md not found. Running setup wizard...'); + + // Prefer the TypeScript wizard (idempotent, detects existing files) + try { + const result = spawnSync(process.execPath, [process.argv[1]!, 'wizard'], { + stdio: 'inherit', + }); + if (result.status === 0 && existsSync(soulPath)) return; + } catch { + // Fall through to legacy init + } + + // Fallback: legacy bash mosaic-init const initBin = join(MOSAIC_HOME, 'tools', '_scripts', 'mosaic-init'); if (existsSync(initBin)) { spawnSync(initBin, [], { stdio: 'inherit' }); } else { - console.error('[mosaic] mosaic-init not found. Run: mosaic wizard'); + console.error('[mosaic] Setup failed. Run: mosaic wizard'); process.exit(1); } } diff --git a/packages/mosaic/__tests__/stages/detect-install.test.ts b/packages/mosaic/__tests__/stages/detect-install.test.ts index bc10659..774256f 100644 --- a/packages/mosaic/__tests__/stages/detect-install.test.ts +++ b/packages/mosaic/__tests__/stages/detect-install.test.ts @@ -65,4 +65,36 @@ describe('detectInstallStage', () => { expect(state.installAction).toBe('keep'); expect(state.soul.agentName).toBe('TestAgent'); }); + + it('pre-populates state when reconfiguring', async () => { + mkdirSync(join(tmpDir, 'bin'), { recursive: true }); + writeFileSync(join(tmpDir, 'SOUL.md'), 'You are **Jarvis** in this session.'); + writeFileSync(join(tmpDir, 'USER.md'), '**Name:** TestUser'); + + const p = new HeadlessPrompter({ + 'What would you like to do?': 'reconfigure', + }); + const state = createState(tmpDir); + await detectInstallStage(p, state, mockConfig); + + expect(state.installAction).toBe('reconfigure'); + // Existing values loaded as defaults for reconfiguration + expect(state.soul.agentName).toBe('TestAgent'); + expect(state.user.userName).toBe('TestUser'); + }); + + it('does not pre-populate state on fresh reset', async () => { + mkdirSync(join(tmpDir, 'bin'), { recursive: true }); + writeFileSync(join(tmpDir, 'SOUL.md'), 'You are **Jarvis** in this session.'); + + const p = new HeadlessPrompter({ + 'What would you like to do?': 'reset', + }); + const state = createState(tmpDir); + await detectInstallStage(p, state, mockConfig); + + expect(state.installAction).toBe('reset'); + // Reset should NOT load existing values + expect(state.soul.agentName).toBeUndefined(); + }); }); diff --git a/packages/mosaic/framework/tools/_scripts/mosaic-init b/packages/mosaic/framework/tools/_scripts/mosaic-init index 3c0b6f7..95ac5e7 100755 --- a/packages/mosaic/framework/tools/_scripts/mosaic-init +++ b/packages/mosaic/framework/tools/_scripts/mosaic-init @@ -60,12 +60,14 @@ Options: --timezone Your timezone (e.g., "America/Chicago") --non-interactive Fail if any required value is missing (no prompts) --soul-only Only generate SOUL.md + --force Overwrite existing files without prompting -h, --help Show help USAGE } NON_INTERACTIVE=0 SOUL_ONLY=0 +FORCE=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -79,6 +81,7 @@ while [[ $# -gt 0 ]]; do --timezone) TIMEZONE="$2"; shift 2 ;; --non-interactive) NON_INTERACTIVE=1; shift ;; --soul-only) SOUL_ONLY=1; shift ;; + --force) FORCE=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; esac @@ -139,6 +142,134 @@ prompt_multiline() { eval "$var_name=\"$value\"" } +# ── Existing file detection ──────────────────────────────────── + +detect_existing_config() { + local found=0 + local existing_files=() + + [[ -f "$SOUL_OUTPUT" ]] && { found=1; existing_files+=("SOUL.md"); } + [[ -f "$USER_OUTPUT" ]] && { found=1; existing_files+=("USER.md"); } + [[ -f "$TOOLS_OUTPUT" ]] && { found=1; existing_files+=("TOOLS.md"); } + + if [[ $found -eq 0 || $FORCE -eq 1 ]]; then + return 0 # No existing files or --force: proceed with fresh install + fi + + echo "[mosaic-init] Existing configuration detected:" + for f in "${existing_files[@]}"; do + echo " ✓ $f" + done + + # Show current agent name if SOUL.md exists + if [[ -f "$SOUL_OUTPUT" ]]; then + local current_name + current_name=$(grep -oP 'You are \*\*\K[^*]+' "$SOUL_OUTPUT" 2>/dev/null || true) + if [[ -n "$current_name" ]]; then + echo " Agent: $current_name" + fi + fi + echo "" + + if [[ $NON_INTERACTIVE -eq 1 ]]; then + echo "[mosaic-init] Existing config found. Use --force to overwrite in non-interactive mode." + exit 0 + fi + + echo "What would you like to do?" + echo " 1) keep — Keep existing files, skip init (default)" + echo " 2) import — Import values from existing files as defaults, then regenerate" + echo " 3) overwrite — Start fresh, overwrite all files" + printf "Choose [1/2/3]: " + read -r choice + + case "${choice:-1}" in + 1|keep) + echo "[mosaic-init] Keeping existing configuration." + # Still push to runtime adapters in case framework was updated + if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then + echo "[mosaic-init] Updating runtime adapters..." + "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" + fi + echo "[mosaic-init] Done. Launch with: mosaic claude" + exit 0 + ;; + 2|import) + echo "[mosaic-init] Importing values from existing files as defaults..." + import_existing_values + ;; + 3|overwrite) + echo "[mosaic-init] Starting fresh install..." + # Back up existing files + local ts + ts=$(date +%Y%m%d%H%M%S) + for f in "${existing_files[@]}"; do + local src="$MOSAIC_HOME/$f" + if [[ -f "$src" ]]; then + cp "$src" "${src}.bak.${ts}" + echo " Backed up $f → ${f}.bak.${ts}" + fi + done + ;; + *) + echo "[mosaic-init] Invalid choice. Keeping existing configuration." + exit 0 + ;; + esac +} + +import_existing_values() { + # Import SOUL.md values + if [[ -f "$SOUL_OUTPUT" ]]; then + local content + content=$(cat "$SOUL_OUTPUT") + + if [[ -z "$AGENT_NAME" ]]; then + AGENT_NAME=$(echo "$content" | grep -oP 'You are \*\*\K[^*]+' 2>/dev/null || true) + fi + if [[ -z "$ROLE_DESCRIPTION" ]]; then + ROLE_DESCRIPTION=$(echo "$content" | grep -oP 'Role identity: \K.+' 2>/dev/null || true) + fi + if [[ -z "$STYLE" ]]; then + if echo "$content" | grep -q 'Be direct, concise'; then + STYLE="direct" + elif echo "$content" | grep -q 'Be warm and conversational'; then + STYLE="friendly" + elif echo "$content" | grep -q 'Use professional, structured'; then + STYLE="formal" + fi + fi + fi + + # Import USER.md values + if [[ -f "$USER_OUTPUT" ]]; then + local content + content=$(cat "$USER_OUTPUT") + + if [[ -z "$USER_NAME" ]]; then + USER_NAME=$(echo "$content" | grep -oP '\*\*Name:\*\* \K.+' 2>/dev/null || true) + fi + if [[ -z "$PRONOUNS" ]]; then + PRONOUNS=$(echo "$content" | grep -oP '\*\*Pronouns:\*\* \K.+' 2>/dev/null || true) + fi + if [[ -z "$TIMEZONE" ]]; then + TIMEZONE=$(echo "$content" | grep -oP '\*\*Timezone:\*\* \K.+' 2>/dev/null || true) + fi + fi + + # Import TOOLS.md values + if [[ -f "$TOOLS_OUTPUT" ]]; then + local content + content=$(cat "$TOOLS_OUTPUT") + + if [[ -z "$CREDENTIALS_LOCATION" ]]; then + CREDENTIALS_LOCATION=$(echo "$content" | grep -oP '\*\*Location:\*\* \K.+' 2>/dev/null || true) + fi + fi +} + +detect_existing_config + # ── SOUL.md Generation ──────────────────────────────────────── echo "[mosaic-init] Generating SOUL.md — agent identity contract" echo "" diff --git a/packages/mosaic/src/stages/detect-install.ts b/packages/mosaic/src/stages/detect-install.ts index 588495e..47ca3fc 100644 --- a/packages/mosaic/src/stages/detect-install.ts +++ b/packages/mosaic/src/stages/detect-install.ts @@ -87,7 +87,9 @@ export async function detectInstallStage( ], }); - if (state.installAction === 'keep') { + if (state.installAction === 'keep' || state.installAction === 'reconfigure') { + // Load existing values — for 'keep' they're final, for 'reconfigure' + // they become pre-populated defaults so the user can tweak them. state.soul = await config.readSoul(); state.user = await config.readUser(); state.tools = await config.readTools(); diff --git a/packages/mosaic/src/stages/soul-setup.ts b/packages/mosaic/src/stages/soul-setup.ts index 5d8fa60..ac06c5e 100644 --- a/packages/mosaic/src/stages/soul-setup.ts +++ b/packages/mosaic/src/stages/soul-setup.ts @@ -24,6 +24,18 @@ export async function soulSetupStage(p: WizardPrompter, state: WizardState): Pro return undefined; }, }); + } else if (state.installAction === 'reconfigure') { + // Show existing value as default so the user can accept or change it + state.soul.agentName = await p.text({ + message: 'What name should agents use?', + placeholder: state.soul.agentName, + defaultValue: state.soul.agentName, + validate: (v) => { + if (v.length === 0) return 'Name cannot be empty'; + if (v.length > 50) return 'Name must be under 50 characters'; + return undefined; + }, + }); } if (state.mode === 'advanced') { @@ -38,7 +50,7 @@ export async function soulSetupStage(p: WizardPrompter, state: WizardState): Pro state.soul.roleDescription ??= DEFAULTS.roleDescription; } - if (!state.soul.communicationStyle) { + if (!state.soul.communicationStyle || state.installAction === 'reconfigure') { state.soul.communicationStyle = await p.select({ message: 'Communication style', options: [ @@ -46,7 +58,7 @@ export async function soulSetupStage(p: WizardPrompter, state: WizardState): Pro { value: 'friendly', label: 'Friendly', hint: 'Warm but efficient, conversational' }, { value: 'formal', label: 'Formal', hint: 'Professional, structured, thorough' }, ], - initialValue: 'direct', + initialValue: state.soul.communicationStyle ?? 'direct', }); } -- 2.49.1 From 07efaa9580862e3d92b942dc7ab7edda5f7b6963 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 2 Apr 2026 20:26:01 -0500 Subject: [PATCH 2/2] chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.3 --- packages/cli/package.json | 2 +- packages/mosaic/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index a8deab1..3414ad6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mosaic/cli", - "version": "0.0.2", + "version": "0.0.3", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 3606e73..16cd384 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -1,6 +1,6 @@ { "name": "@mosaic/mosaic", - "version": "0.0.2", + "version": "0.0.3", "description": "Mosaic agent framework — installation wizard and meta package", "type": "module", "main": "dist/index.js", -- 2.49.1