419 lines
11 KiB
TypeScript
419 lines
11 KiB
TypeScript
/**
|
|
* Config CLI command handlers.
|
|
*
|
|
* Handles `omp config <command>` subcommands for managing settings.
|
|
* Uses the settings schema as the source of truth for available settings.
|
|
*/
|
|
|
|
import { APP_NAME, getAgentDir } from "@oh-my-pi/pi-utils";
|
|
import chalk from "chalk";
|
|
import {
|
|
getDefault,
|
|
getEnumValues,
|
|
getType,
|
|
getUi,
|
|
type SettingPath,
|
|
Settings,
|
|
type SettingValue,
|
|
settings,
|
|
} from "../config/settings";
|
|
import { SETTINGS_SCHEMA } from "../config/settings-schema";
|
|
import { theme } from "../modes/theme/theme";
|
|
import { initXdg } from "./commands/init-xdg";
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
export type ConfigAction = "list" | "get" | "set" | "reset" | "path" | "init-xdg";
|
|
|
|
export interface ConfigCommandArgs {
|
|
action: ConfigAction;
|
|
key?: string;
|
|
value?: string;
|
|
flags: {
|
|
json?: boolean;
|
|
};
|
|
}
|
|
// =============================================================================
|
|
// Setting Filtering
|
|
// =============================================================================
|
|
|
|
type CliSettingDef = {
|
|
path: SettingPath;
|
|
type: string;
|
|
description: string;
|
|
tab: string;
|
|
};
|
|
|
|
const ALL_SETTING_PATHS = Object.keys(SETTINGS_SCHEMA) as SettingPath[];
|
|
|
|
/** Find setting definition by path */
|
|
function findSettingDef(path: string): CliSettingDef | undefined {
|
|
if (!(path in SETTINGS_SCHEMA)) return undefined;
|
|
const key = path as SettingPath;
|
|
const ui = getUi(key);
|
|
return {
|
|
path: key,
|
|
type: getType(key),
|
|
description: ui?.description ?? "",
|
|
tab: ui?.tab ?? "internal",
|
|
};
|
|
}
|
|
|
|
/** Get available values for a setting */
|
|
function getSettingValues(def: CliSettingDef): readonly string[] | undefined {
|
|
if (def.type === "enum") {
|
|
return getEnumValues(def.path);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Argument Parser
|
|
// =============================================================================
|
|
|
|
const VALID_ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path", "init-xdg"];
|
|
|
|
/**
|
|
* Parse config subcommand arguments.
|
|
* Returns undefined if not a config command.
|
|
*/
|
|
export function parseConfigArgs(args: string[]): ConfigCommandArgs | undefined {
|
|
if (args.length === 0 || args[0] !== "config") {
|
|
return undefined;
|
|
}
|
|
|
|
if (args.length < 2 || args[1] === "--help" || args[1] === "-h") {
|
|
return { action: "list", flags: {} };
|
|
}
|
|
|
|
const action = args[1];
|
|
if (!VALID_ACTIONS.includes(action as ConfigAction)) {
|
|
console.error(chalk.red(`Unknown config command: ${action}`));
|
|
console.error(`Valid commands: ${VALID_ACTIONS.join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const result: ConfigCommandArgs = {
|
|
action: action as ConfigAction,
|
|
flags: {},
|
|
};
|
|
|
|
const positionalArgs: string[] = [];
|
|
for (let i = 2; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === "--json") {
|
|
result.flags.json = true;
|
|
} else if (!arg.startsWith("-")) {
|
|
positionalArgs.push(arg);
|
|
}
|
|
}
|
|
|
|
if (positionalArgs.length > 0) {
|
|
result.key = positionalArgs[0];
|
|
}
|
|
if (positionalArgs.length > 1) {
|
|
result.value = positionalArgs.slice(1).join(" ");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Value Formatting
|
|
// =============================================================================
|
|
|
|
function formatValue(value: unknown): string {
|
|
if (value === undefined || value === null) {
|
|
return chalk.dim("(not set)");
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return value ? chalk.green("true") : chalk.red("false");
|
|
}
|
|
if (typeof value === "number") {
|
|
return chalk.cyan(String(value));
|
|
}
|
|
if (typeof value === "string") {
|
|
return chalk.yellow(value);
|
|
}
|
|
if (Array.isArray(value) || typeof value === "object") {
|
|
try {
|
|
return chalk.yellow(JSON.stringify(value));
|
|
} catch {
|
|
return chalk.yellow(String(value));
|
|
}
|
|
}
|
|
return chalk.yellow(String(value));
|
|
}
|
|
|
|
function getTypeDisplay(def: CliSettingDef): string {
|
|
const values = getSettingValues(def);
|
|
if (values && values.length > 0) {
|
|
return `(${values.join("|")})`;
|
|
}
|
|
switch (def.type) {
|
|
case "boolean":
|
|
return "(boolean)";
|
|
case "number":
|
|
return "(number)";
|
|
case "array":
|
|
return "(array)";
|
|
case "record":
|
|
return "(record)";
|
|
default:
|
|
return "(string)";
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Schema-Driven Value Parsing
|
|
// =============================================================================
|
|
|
|
function parseAndSetValue(path: SettingPath, rawValue: string): void {
|
|
const schemaType = getType(path);
|
|
let parsedValue: unknown;
|
|
|
|
const trimmed = rawValue.trim();
|
|
switch (schemaType) {
|
|
case "boolean": {
|
|
const lower = trimmed.toLowerCase();
|
|
if (["true", "1", "yes", "on"].includes(lower)) parsedValue = true;
|
|
else if (["false", "0", "no", "off"].includes(lower)) parsedValue = false;
|
|
else throw new Error(`Invalid boolean value: ${rawValue}. Use true/false, yes/no, on/off, or 1/0`);
|
|
break;
|
|
}
|
|
case "number":
|
|
parsedValue = Number(trimmed);
|
|
if (!Number.isFinite(parsedValue)) throw new Error(`Invalid number: ${rawValue}`);
|
|
break;
|
|
case "enum": {
|
|
const valid = getEnumValues(path);
|
|
if (valid && !valid.includes(trimmed)) {
|
|
throw new Error(`Invalid value: ${rawValue}. Valid values: ${valid.join(", ")}`);
|
|
}
|
|
parsedValue = trimmed;
|
|
break;
|
|
}
|
|
case "array": {
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(trimmed);
|
|
} catch {
|
|
throw new Error(`Invalid array JSON: ${rawValue}`);
|
|
}
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error(`Invalid array JSON: ${rawValue}`);
|
|
}
|
|
parsedValue = parsed;
|
|
break;
|
|
}
|
|
case "record": {
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(trimmed);
|
|
} catch {
|
|
throw new Error(`Invalid record JSON: ${rawValue}`);
|
|
}
|
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
throw new Error(`Invalid record JSON: ${rawValue}`);
|
|
}
|
|
parsedValue = parsed;
|
|
break;
|
|
}
|
|
default:
|
|
parsedValue = trimmed;
|
|
}
|
|
|
|
settings.set(path, parsedValue as SettingValue<typeof path>);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Command Handlers
|
|
// =============================================================================
|
|
|
|
export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
|
|
await Settings.init();
|
|
|
|
switch (cmd.action) {
|
|
case "list":
|
|
handleList(cmd.flags);
|
|
break;
|
|
case "get":
|
|
handleGet(cmd.key, cmd.flags);
|
|
break;
|
|
case "set":
|
|
await handleSet(cmd.key, cmd.value, cmd.flags);
|
|
break;
|
|
case "reset":
|
|
await handleReset(cmd.key, cmd.flags);
|
|
break;
|
|
case "path":
|
|
handlePath();
|
|
break;
|
|
case "init-xdg":
|
|
await initXdg();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleList(flags: { json?: boolean }): void {
|
|
const defs = ALL_SETTING_PATHS.map(path => findSettingDef(path)).filter((def): def is CliSettingDef => !!def);
|
|
|
|
if (flags.json) {
|
|
const result: Record<string, { value: unknown; type: string; description: string }> = {};
|
|
for (const def of defs) {
|
|
result[def.path] = {
|
|
value: settings.get(def.path),
|
|
type: def.type,
|
|
description: def.description,
|
|
};
|
|
}
|
|
console.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log(chalk.bold("Settings:\n"));
|
|
|
|
const groups: Record<string, CliSettingDef[]> = {};
|
|
for (const def of defs) {
|
|
if (!groups[def.tab]) {
|
|
groups[def.tab] = [];
|
|
}
|
|
groups[def.tab].push(def);
|
|
}
|
|
|
|
const sortedGroups = Object.keys(groups).sort((a, b) => {
|
|
if (a === "config") return -1;
|
|
if (b === "config") return 1;
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
for (const group of sortedGroups) {
|
|
console.log(chalk.bold.blue(`[${group}]`));
|
|
for (const def of groups[group]) {
|
|
const value = settings.get(def.path);
|
|
const valueStr = formatValue(value);
|
|
const typeStr = getTypeDisplay(def);
|
|
console.log(` ${chalk.white(def.path)} = ${valueStr} ${chalk.dim(typeStr)}`);
|
|
}
|
|
console.log("");
|
|
}
|
|
}
|
|
|
|
function handleGet(key: string | undefined, flags: { json?: boolean }): void {
|
|
if (!key) {
|
|
console.error(chalk.red(`Usage: ${APP_NAME} config get <key>`));
|
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
process.exit(1);
|
|
}
|
|
|
|
const def = findSettingDef(key);
|
|
if (!def) {
|
|
console.error(chalk.red(`Unknown setting: ${key}`));
|
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
process.exit(1);
|
|
}
|
|
|
|
const value = settings.get(def.path);
|
|
|
|
if (flags.json) {
|
|
console.log(JSON.stringify({ key: def.path, value, type: def.type, description: def.description }, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log(formatValue(value));
|
|
}
|
|
|
|
async function handleSet(key: string | undefined, value: string | undefined, flags: { json?: boolean }): Promise<void> {
|
|
if (!key || value === undefined) {
|
|
console.error(chalk.red(`Usage: ${APP_NAME} config set <key> <value>`));
|
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
process.exit(1);
|
|
}
|
|
|
|
const def = findSettingDef(key);
|
|
if (!def) {
|
|
console.error(chalk.red(`Unknown setting: ${key}`));
|
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
parseAndSetValue(def.path, value);
|
|
} catch (err) {
|
|
console.error(chalk.red(String(err)));
|
|
process.exit(1);
|
|
}
|
|
|
|
const newValue = settings.get(def.path);
|
|
|
|
if (flags.json) {
|
|
console.log(JSON.stringify({ key: def.path, value: newValue }));
|
|
} else {
|
|
console.log(chalk.green(`${theme.status.success} Set ${def.path} = ${formatValue(newValue)}`));
|
|
}
|
|
}
|
|
|
|
async function handleReset(key: string | undefined, flags: { json?: boolean }): Promise<void> {
|
|
if (!key) {
|
|
console.error(chalk.red(`Usage: ${APP_NAME} config reset <key>`));
|
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
process.exit(1);
|
|
}
|
|
|
|
const def = findSettingDef(key);
|
|
if (!def) {
|
|
console.error(chalk.red(`Unknown setting: ${key}`));
|
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
process.exit(1);
|
|
}
|
|
|
|
const path = def.path as SettingPath;
|
|
const defaultValue = getDefault(path);
|
|
settings.set(path, defaultValue as SettingValue<typeof path>);
|
|
|
|
if (flags.json) {
|
|
console.log(JSON.stringify({ key: def.path, value: defaultValue }));
|
|
} else {
|
|
console.log(chalk.green(`${theme.status.success} Reset ${def.path} to ${formatValue(defaultValue)}`));
|
|
}
|
|
}
|
|
|
|
function handlePath(): void {
|
|
console.log(getAgentDir());
|
|
}
|
|
|
|
// =============================================================================
|
|
// Help
|
|
// =============================================================================
|
|
|
|
export function printConfigHelp(): void {
|
|
console.log(`${chalk.bold(`${APP_NAME} config`)} - Manage settings
|
|
|
|
${chalk.bold("Commands:")}
|
|
list List all settings with current values
|
|
get <key> Get a specific setting value
|
|
set <key> <value> Set a setting value
|
|
reset <key> Reset a setting to its default value
|
|
path Print the config directory path
|
|
init-xdg Initialize XDG Base Directory structure
|
|
|
|
${chalk.bold("Options:")}
|
|
--json Output as JSON
|
|
|
|
${chalk.bold("Examples:")}
|
|
${APP_NAME} config list
|
|
${APP_NAME} config get theme
|
|
${APP_NAME} config set theme catppuccin-mocha
|
|
${APP_NAME} config set compaction.enabled false
|
|
${APP_NAME} config set defaultThinkingLevel medium
|
|
${APP_NAME} config reset steeringMode
|
|
${APP_NAME} config list --json
|
|
${APP_NAME} config init-xdg
|
|
|
|
${chalk.bold("Boolean Values:")}
|
|
true, false, yes, no, on, off, 1, 0
|
|
`);
|
|
}
|