Compare commits

...

11 Commits

Author SHA1 Message Date
Jarvis
a08da9164a chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.4
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-04-02 20:49:09 -05:00
Jarvis
e83674ac51 fix: mosaic sync — auto-stash dirty worktree before pull --rebase
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
git pull --rebase fails with 'cannot pull with rebase: You have
unstaged changes' when the skills repo has local modifications.

Fix: detect dirty index/worktree, stash before pull, restore after.
Also gracefully handle pull failures (warn and continue with existing
checkout) and stash pop conflicts.
2026-04-02 20:41:11 -05:00
Jarvis
a6e59bf829 fix: syncDirectory — guard same-path copy and skip nested .git dirs
Two bugs causing 'EACCES: permission denied, copyfile' when source
and target are the same path (e.g. wizard with sourceDir == mosaicHome):

1. No same-path guard — syncDirectory tried to copy every file onto
   itself; git pack files are read-only (0444) so copyFileSync fails.
2. excludeGit only matched top-level .git — nested .git dirs like
   sources/agent-skills/.git were copied, hitting the same permission
   issue.

Fixes:
- Early return when resolve(source) === resolve(target)
- Match .git dirs at any depth via dirName and relPath checks
- Skip files inside .git/ paths

Added file-ops.test.ts with 4 tests covering all cases.
2026-04-02 20:41:11 -05:00
e46f0641f6 Merge pull request 'fix: make mosaic init idempotent — detect existing config files' (#355) from fix/idempotent-init into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#355
2026-04-03 01:30:01 +00:00
Jarvis
07efaa9580 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.3
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
2026-04-02 20:26:01 -05:00
Jarvis
361fece023 fix: make mosaic init idempotent — detect existing config files
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
- 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.
2026-04-02 20:20:59 -05:00
80e69016b0 Merge pull request 'chore: bump all packages to 0.0.2 — drop alpha prerelease tag' (#354) from chore/bump-0.0.2 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#354
2026-04-03 01:12:24 +00:00
Jarvis
e084a88a9d chore: bump all packages to 0.0.2 — drop alpha prerelease tag
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Switches from 0.0.1-alpha.2 to 0.0.2. Clean semver, no prerelease
suffixes. We're still alpha (0.0.x range).
2026-04-02 20:03:55 -05:00
990a88362f Merge pull request 'feat: complete CLI command parity — coord, prdy, seq, upgrade' (#352) from fix/complete-cli-parity into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#352
2026-04-03 00:52:36 +00:00
Jarvis
ea9782b2dc feat: complete CLI command parity — add coord, prdy, seq, upgrade
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The #351 merge landed before the force-push with full commands.
This adds the missing subcommands:

- mosaic coord {init,status,mission,continue,run,smoke,resume}
  → delegates to tools/orchestrator/*.sh with --claude/--codex/--pi/--yolo
- mosaic prdy {init,update,validate,status}
  → delegates to tools/prdy/*.sh with --claude/--codex/--pi
- mosaic seq {check,fix,start}
  → sequential-thinking MCP management (native TS)
- mosaic upgrade {release,check,project}
  → delegates to tools/_scripts/mosaic-release-upgrade and mosaic-upgrade

Also removes duplicate prdy registration (was in both launch.ts and
the old registerPrdyCommand — now only in launch.ts).
2026-04-02 19:51:34 -05:00
8efbaf100e Merge pull request 'feat: unify mosaic CLI — single binary, no PATH conflict' (#351) from feat/unify-mosaic-cli into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 00:41:06 +00:00
32 changed files with 557 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/gateway",
"version": "0.0.0",
"version": "0.0.2",
"private": true,
"type": "module",
"main": "dist/main.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/web",
"version": "0.0.0",
"version": "0.0.2",
"private": true,
"scripts": {
"build": "next build",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/agent",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/auth",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/brain",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/cli",
"version": "0.0.1-alpha.2",
"version": "0.0.4",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -5,7 +5,7 @@ import { Command } from 'commander';
import { createQualityRailsCli } from '@mosaic/quality-rails';
import { registerAgentCommand } from './commands/agent.js';
import { registerMissionCommand } from './commands/mission.js';
import { registerPrdyCommand } from './commands/prdy.js';
// prdy is registered via launch.ts
import { registerLaunchCommands } from './commands/launch.js';
const _require = createRequire(import.meta.url);
@@ -298,10 +298,6 @@ registerAgentCommand(program);
registerMissionCommand(program);
// ─── prdy ──────────────────────────────────────────────────────────────
registerPrdyCommand(program);
// ─── quality-rails ──────────────────────────────────────────────────────
const qrWrapper = createQualityRailsCli();

View File

@@ -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);
}
}
@@ -461,23 +473,191 @@ function execRuntime(cmd: string, args: string[]): void {
}
}
// ─── Framework script delegation (for tools that remain in bash) ─────────────
// ─── Framework script/tool delegation ───────────────────────────────────────
function delegateToFrameworkScript(script: string, args: string[]): never {
const scriptPath = join(MOSAIC_HOME, 'tools', '_scripts', script);
function delegateToScript(scriptPath: string, args: string[], env?: Record<string, string>): never {
if (!existsSync(scriptPath)) {
console.error(`[mosaic] Script not found: ${scriptPath}`);
process.exit(1);
}
try {
execFileSync(scriptPath, args, { stdio: 'inherit' });
execFileSync('bash', [scriptPath, ...args], {
stdio: 'inherit',
env: { ...process.env, ...env },
});
process.exit(0);
} catch (err) {
process.exit((err as { status?: number }).status ?? 1);
}
}
// ─── Commander registration ──────────────────────────────────────────────────
function fwScript(name: string): string {
return join(MOSAIC_HOME, 'tools', '_scripts', name);
}
function toolScript(toolDir: string, name: string): string {
return join(MOSAIC_HOME, 'tools', toolDir, name);
}
// ─── Coord (mission orchestrator) ───────────────────────────────────────────
const COORD_SUBCMDS: Record<string, string> = {
status: 'session-status.sh',
session: 'session-status.sh',
init: 'mission-init.sh',
mission: 'mission-status.sh',
progress: 'mission-status.sh',
continue: 'continue-prompt.sh',
next: 'continue-prompt.sh',
run: 'session-run.sh',
start: 'session-run.sh',
smoke: 'smoke-test.sh',
test: 'smoke-test.sh',
resume: 'session-resume.sh',
recover: 'session-resume.sh',
};
function runCoord(args: string[]): never {
checkMosaicHome();
let runtime = 'claude';
let yoloFlag = '';
const coordArgs: string[] = [];
for (const arg of args) {
if (arg === '--claude' || arg === '--codex' || arg === '--pi') {
runtime = arg.slice(2);
} else if (arg === '--yolo') {
yoloFlag = '--yolo';
} else {
coordArgs.push(arg);
}
}
const subcmd = coordArgs[0] ?? 'help';
const subArgs = coordArgs.slice(1);
const script = COORD_SUBCMDS[subcmd];
if (!script) {
console.log(`mosaic coord — mission coordinator tools
Commands:
init --name <name> [opts] Initialize a new mission
mission [--project <path>] Show mission progress dashboard
status [--project <path>] Check agent session health
continue [--project <path>] Generate continuation prompt
run [--project <path>] Launch runtime with mission context
smoke Run orchestration smoke checks
resume [--project <path>] Crash recovery
Runtime: --claude (default) | --codex | --pi | --yolo`);
process.exit(subcmd === 'help' ? 0 : 1);
}
if (yoloFlag) subArgs.unshift(yoloFlag);
delegateToScript(toolScript('orchestrator', script), subArgs, {
MOSAIC_COORD_RUNTIME: runtime,
});
}
// ─── Prdy (PRD tools via framework scripts) ─────────────────────────────────
const PRDY_SUBCMDS: Record<string, string> = {
init: 'prdy-init.sh',
update: 'prdy-update.sh',
validate: 'prdy-validate.sh',
check: 'prdy-validate.sh',
status: 'prdy-status.sh',
};
function runPrdyLocal(args: string[]): never {
checkMosaicHome();
let runtime = 'claude';
const prdyArgs: string[] = [];
for (const arg of args) {
if (arg === '--claude' || arg === '--codex' || arg === '--pi') {
runtime = arg.slice(2);
} else {
prdyArgs.push(arg);
}
}
const subcmd = prdyArgs[0] ?? 'help';
const subArgs = prdyArgs.slice(1);
const script = PRDY_SUBCMDS[subcmd];
if (!script) {
console.log(`mosaic prdy — PRD creation and validation
Commands:
init [--project <path>] [--name <feature>] Create docs/PRD.md
update [--project <path>] Update existing PRD
validate [--project <path>] Check PRD completeness
status [--project <path>] Quick PRD health check
Runtime: --claude (default) | --codex | --pi`);
process.exit(subcmd === 'help' ? 0 : 1);
}
delegateToScript(toolScript('prdy', script), subArgs, {
MOSAIC_PRDY_RUNTIME: runtime,
});
}
// ─── Seq (sequential-thinking MCP) ──────────────────────────────────────────
function runSeq(args: string[]): never {
checkMosaicHome();
const action = args[0] ?? 'check';
const rest = args.slice(1);
const checker = fwScript('mosaic-ensure-sequential-thinking');
switch (action) {
case 'check':
delegateToScript(checker, ['--check', ...rest]);
break; // unreachable
case 'fix':
case 'apply':
delegateToScript(checker, rest);
break;
case 'start': {
console.log('[mosaic] Starting sequential-thinking MCP server...');
try {
execFileSync('npx', ['-y', '@modelcontextprotocol/server-sequential-thinking', ...rest], {
stdio: 'inherit',
});
process.exit(0);
} catch (err) {
process.exit((err as { status?: number }).status ?? 1);
}
break;
}
default:
console.error(`[mosaic] Unknown seq subcommand '${action}'. Use: check|fix|start`);
process.exit(1);
}
}
// ─── Upgrade ────────────────────────────────────────────────────────────────
function runUpgrade(args: string[]): never {
checkMosaicHome();
const subcmd = args[0];
if (!subcmd || subcmd === 'release') {
delegateToScript(fwScript('mosaic-release-upgrade'), args.slice(subcmd === 'release' ? 1 : 0));
} else if (subcmd === 'check') {
delegateToScript(fwScript('mosaic-release-upgrade'), ['--dry-run', ...args.slice(1)]);
} else if (subcmd === 'project') {
delegateToScript(fwScript('mosaic-upgrade'), args.slice(1));
} else if (subcmd.startsWith('-')) {
delegateToScript(fwScript('mosaic-release-upgrade'), args);
} else {
delegateToScript(fwScript('mosaic-upgrade'), args);
}
}
// ─── Commander registration ─────────────────────────────────────────────────
export function registerLaunchCommands(program: Command): void {
// Runtime launchers
@@ -509,15 +689,58 @@ export function registerLaunchCommands(program: Command): void {
launchRuntime(runtime as RuntimeName, cmd.args, true);
});
// Framework management commands (delegate to bash scripts)
const frameworkCommands: Record<string, { desc: string; script: string }> = {
// Coord (mission orchestrator)
program
.command('coord')
.description('Mission coordinator tools (init, status, run, continue, resume)')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runCoord(cmd.args);
});
// Prdy (PRD tools via local framework scripts)
program
.command('prdy')
.description('PRD creation and validation (init, update, validate, status)')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runPrdyLocal(cmd.args);
});
// Seq (sequential-thinking MCP management)
program
.command('seq')
.description('sequential-thinking MCP management (check/fix/start)')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runSeq(cmd.args);
});
// Upgrade (release + project)
program
.command('upgrade')
.description('Upgrade Mosaic release or project files')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runUpgrade(cmd.args);
});
// Direct framework script delegates
const directCommands: Record<string, { desc: string; script: string }> = {
init: { desc: 'Generate SOUL.md (agent identity contract)', script: 'mosaic-init' },
doctor: { desc: 'Health audit — detect drift and missing files', script: 'mosaic-doctor' },
sync: { desc: 'Sync skills from canonical source', script: 'mosaic-sync-skills' },
bootstrap: { desc: 'Bootstrap a repo with Mosaic standards', script: 'mosaic-bootstrap-repo' },
bootstrap: {
desc: 'Bootstrap a repo with Mosaic standards',
script: 'mosaic-bootstrap-repo',
},
};
for (const [name, { desc, script }] of Object.entries(frameworkCommands)) {
for (const [name, { desc, script }] of Object.entries(directCommands)) {
program
.command(name)
.description(desc)
@@ -525,7 +748,7 @@ export function registerLaunchCommands(program: Command): void {
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
checkMosaicHome();
delegateToFrameworkScript(script, cmd.args);
delegateToScript(fwScript(script), cmd.args);
});
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/coord",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/db",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/design-tokens",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/forge",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/log",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/macp",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/memory",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -0,0 +1,88 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
mkdtempSync,
mkdirSync,
writeFileSync,
readFileSync,
existsSync,
chmodSync,
rmSync,
} from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { syncDirectory } from '../../src/platform/file-ops.js';
describe('syncDirectory', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-file-ops-'));
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
it('is a no-op when source and target are the same path', () => {
const dir = join(tmpDir, 'same');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'file.txt'), 'hello');
// Should not throw even with read-only files
const gitDir = join(dir, '.git', 'objects', 'pack');
mkdirSync(gitDir, { recursive: true });
const packFile = join(gitDir, 'pack-abc.idx');
writeFileSync(packFile, 'data');
chmodSync(packFile, 0o444);
expect(() => syncDirectory(dir, dir)).not.toThrow();
});
it('skips nested .git directories when excludeGit is true', () => {
const src = join(tmpDir, 'src');
const dest = join(tmpDir, 'dest');
// Create source with a nested .git
mkdirSync(join(src, 'sources', 'skills', '.git', 'objects'), { recursive: true });
writeFileSync(join(src, 'sources', 'skills', '.git', 'objects', 'pack.idx'), 'git-data');
writeFileSync(join(src, 'sources', 'skills', 'SKILL.md'), 'skill content');
writeFileSync(join(src, 'README.md'), 'readme');
syncDirectory(src, dest, { excludeGit: true });
// .git contents should NOT be copied
expect(existsSync(join(dest, 'sources', 'skills', '.git'))).toBe(false);
// Normal files should be copied
expect(readFileSync(join(dest, 'sources', 'skills', 'SKILL.md'), 'utf-8')).toBe(
'skill content',
);
expect(readFileSync(join(dest, 'README.md'), 'utf-8')).toBe('readme');
});
it('copies nested .git directories when excludeGit is false', () => {
const src = join(tmpDir, 'src');
const dest = join(tmpDir, 'dest');
mkdirSync(join(src, 'sub', '.git'), { recursive: true });
writeFileSync(join(src, 'sub', '.git', 'HEAD'), 'ref: refs/heads/main');
syncDirectory(src, dest, { excludeGit: false });
expect(readFileSync(join(dest, 'sub', '.git', 'HEAD'), 'utf-8')).toBe('ref: refs/heads/main');
});
it('respects preserve option', () => {
const src = join(tmpDir, 'src');
const dest = join(tmpDir, 'dest');
mkdirSync(src, { recursive: true });
mkdirSync(dest, { recursive: true });
writeFileSync(join(src, 'SOUL.md'), 'new soul');
writeFileSync(join(dest, 'SOUL.md'), 'old soul');
writeFileSync(join(src, 'README.md'), 'new readme');
syncDirectory(src, dest, { preserve: ['SOUL.md'] });
expect(readFileSync(join(dest, 'SOUL.md'), 'utf-8')).toBe('old soul');
expect(readFileSync(join(dest, 'README.md'), 'utf-8')).toBe('new readme');
});
});

View File

@@ -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();
});
});

View File

@@ -60,12 +60,14 @@ Options:
--timezone <tz> 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 ""

View File

@@ -55,7 +55,26 @@ mkdir -p "$MOSAIC_HOME" "$MOSAIC_SKILLS_DIR" "$MOSAIC_LOCAL_SKILLS_DIR"
if [[ $fetch -eq 1 ]]; then
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
git -C "$SKILLS_REPO_DIR" pull --rebase
# Stash any local changes (dirty index or worktree) before pulling
local_changes=0
if ! git -C "$SKILLS_REPO_DIR" diff --quiet 2>/dev/null || \
! git -C "$SKILLS_REPO_DIR" diff --cached --quiet 2>/dev/null; then
local_changes=1
echo "[mosaic-skills] Stashing local changes..."
git -C "$SKILLS_REPO_DIR" stash push -q -m "mosaic-sync-skills auto-stash"
fi
if ! git -C "$SKILLS_REPO_DIR" pull --rebase; then
echo "[mosaic-skills] WARN: pull failed — continuing with existing checkout" >&2
fi
# Restore stashed changes
if [[ $local_changes -eq 1 ]]; then
echo "[mosaic-skills] Restoring local changes..."
git -C "$SKILLS_REPO_DIR" stash pop -q 2>/dev/null || \
echo "[mosaic-skills] WARN: stash pop had conflicts — check $SKILLS_REPO_DIR" >&2
fi
else
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"
mkdir -p "$(dirname "$SKILLS_REPO_DIR")"

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/mosaic",
"version": "0.0.1-alpha.2",
"version": "0.0.4",
"description": "Mosaic agent framework — installation wizard and meta package",
"type": "module",
"main": "dist/index.js",

View File

@@ -1,7 +1,7 @@
import { homedir } from 'node:os';
import { join } from 'node:path';
export const VERSION = '0.1.0';
export const VERSION = '0.0.2';
export const DEFAULT_MOSAIC_HOME = join(homedir(), '.config', 'mosaic');

View File

@@ -9,7 +9,7 @@ import {
unlinkSync,
statSync,
} from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { dirname, join, relative, resolve } from 'node:path';
const MAX_BACKUPS = 3;
@@ -68,6 +68,9 @@ export function syncDirectory(
target: string,
options: { preserve?: string[]; excludeGit?: boolean } = {},
): void {
// Guard: source and target are the same directory — nothing to sync
if (resolve(source) === resolve(target)) return;
const preserveSet = new Set(options.preserve ?? []);
// Collect files from source
@@ -77,9 +80,10 @@ export function syncDirectory(
const stat = statSync(src);
if (stat.isDirectory()) {
const relPath = relative(relBase, src);
const dirName = relPath.split('/').pop() ?? '';
// Skip .git
if (options.excludeGit && relPath === '.git') return;
// Skip any .git directory (top-level or nested, e.g. sources/agent-skills/.git)
if (options.excludeGit && (dirName === '.git' || relPath.includes('/.git'))) return;
// Skip preserved paths at top level
if (preserveSet.has(relPath) && existsSync(dest)) return;
@@ -91,6 +95,9 @@ export function syncDirectory(
} else {
const relPath = relative(relBase, src);
// Skip files inside .git directories
if (options.excludeGit && relPath.includes('/.git/')) return;
// Skip preserved files at top level
if (preserveSet.has(relPath) && existsSync(dest)) return;

View File

@@ -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();

View File

@@ -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<CommunicationStyle>({
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',
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/prdy",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/quality-rails",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/queue",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/types",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/discord-plugin",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/oc-macp-plugin",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "src/index.ts",
"description": "OpenClaw ACP runtime backend that routes sessions_spawn(runtime:\"macp\") to the Pi MACP runner.",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/oc-framework-plugin",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"type": "module",
"main": "src/index.ts",
"description": "Injects Mosaic framework rails, runtime contract, and active mission context into all OpenClaw agent sessions and ACP subagent spawns.",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/telegram-plugin",
"version": "0.0.1-alpha.2",
"version": "0.0.2",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {