feat(mosaic): add top-level mosaic config command (CU-04-04, CU-04-05)
Adds `mosaic config` command tree with five subcommands (show, get, set, edit, path) backed by ConfigService; adds minimal get/set/path/readAll primitives to ConfigService + FileConfigAdapter. Includes Vitest tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { readFileSync, existsSync, readdirSync, statSync, copyFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { ConfigService } from './config-service.js';
|
||||
import type { ConfigService, ConfigSection, ResolvedConfig } from './config-service.js';
|
||||
import type { SoulConfig, UserConfig, ToolsConfig, InstallAction } from '../types.js';
|
||||
import { soulSchema, userSchema, toolsSchema } from './schemas.js';
|
||||
import { renderTemplate } from '../template/engine.js';
|
||||
@@ -159,6 +159,73 @@ export class FileConfigAdapter implements ConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
async readAll(): Promise<ResolvedConfig> {
|
||||
const [soul, user, tools] = await Promise.all([
|
||||
this.readSoul(),
|
||||
this.readUser(),
|
||||
this.readTools(),
|
||||
]);
|
||||
return { soul, user, tools };
|
||||
}
|
||||
|
||||
async getValue(dottedKey: string): Promise<unknown> {
|
||||
const parts = dottedKey.split('.');
|
||||
const section = parts[0] ?? '';
|
||||
const field = parts.slice(1).join('.');
|
||||
const config = await this.readAll();
|
||||
if (!this.isValidSection(section)) return undefined;
|
||||
const sectionData = config[section as ConfigSection] as Record<string, unknown>;
|
||||
return field ? sectionData[field] : sectionData;
|
||||
}
|
||||
|
||||
async setValue(dottedKey: string, value: string): Promise<unknown> {
|
||||
const parts = dottedKey.split('.');
|
||||
const section = parts[0] ?? '';
|
||||
const field = parts.slice(1).join('.');
|
||||
if (!this.isValidSection(section) || !field) {
|
||||
throw new Error(
|
||||
`Invalid key "${dottedKey}". Use format <section>.<field> (e.g. soul.agentName).`,
|
||||
);
|
||||
}
|
||||
|
||||
const previous = await this.getValue(dottedKey);
|
||||
|
||||
if (section === 'soul') {
|
||||
const current = await this.readSoul();
|
||||
await this.writeSoul({ ...current, [field]: value });
|
||||
} else if (section === 'user') {
|
||||
const current = await this.readUser();
|
||||
await this.writeUser({ ...current, [field]: value });
|
||||
} else {
|
||||
const current = await this.readTools();
|
||||
await this.writeTools({ ...current, [field]: value });
|
||||
}
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
getConfigPath(section?: ConfigSection): string {
|
||||
if (!section) return this.mosaicHome;
|
||||
const fileMap: Record<ConfigSection, string> = {
|
||||
soul: join(this.mosaicHome, 'SOUL.md'),
|
||||
user: join(this.mosaicHome, 'USER.md'),
|
||||
tools: join(this.mosaicHome, 'TOOLS.md'),
|
||||
};
|
||||
return fileMap[section];
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
return (
|
||||
existsSync(join(this.mosaicHome, 'SOUL.md')) ||
|
||||
existsSync(join(this.mosaicHome, 'USER.md')) ||
|
||||
existsSync(join(this.mosaicHome, 'TOOLS.md'))
|
||||
);
|
||||
}
|
||||
|
||||
private isValidSection(s: string): s is ConfigSection {
|
||||
return s === 'soul' || s === 'user' || s === 'tools';
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for template in source dir first, then mosaic home.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user