Compare commits
11 Commits
15830e2f2a
...
fix/idempo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08da9164a | ||
|
|
e83674ac51 | ||
|
|
a6e59bf829 | ||
| e46f0641f6 | |||
|
|
07efaa9580 | ||
|
|
361fece023 | ||
| 80e69016b0 | |||
|
|
e084a88a9d | ||
| 990a88362f | |||
|
|
ea9782b2dc | ||
| 8efbaf100e |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mosaic/gateway",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/main.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mosaic/web",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
88
packages/mosaic/__tests__/platform/file-ops.test.ts
Normal file
88
packages/mosaic/__tests__/platform/file-ops.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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")"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user