@fission-ai/openspec
Version:
AI-native system for spec-driven development
200 lines • 6.26 kB
JavaScript
import { z } from 'zod';
/**
* Zod schema for global OpenSpec configuration.
* Uses passthrough() to preserve unknown fields for forward compatibility.
*/
export const GlobalConfigSchema = z
.object({
featureFlags: z
.record(z.string(), z.boolean())
.optional()
.default({}),
})
.passthrough();
/**
* Default configuration values.
*/
export const DEFAULT_CONFIG = {
featureFlags: {},
};
const KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(DEFAULT_CONFIG));
/**
* Validate a config key path for CLI set operations.
* Unknown top-level keys are rejected unless explicitly allowed by the caller.
*/
export function validateConfigKeyPath(path) {
const rawKeys = path.split('.');
if (rawKeys.length === 0 || rawKeys.some((key) => key.trim() === '')) {
return { valid: false, reason: 'Key path must not be empty' };
}
const rootKey = rawKeys[0];
if (!KNOWN_TOP_LEVEL_KEYS.has(rootKey)) {
return { valid: false, reason: `Unknown top-level key "${rootKey}"` };
}
if (rootKey === 'featureFlags') {
if (rawKeys.length > 2) {
return { valid: false, reason: 'featureFlags values are booleans and do not support nested keys' };
}
return { valid: true };
}
if (rawKeys.length > 1) {
return { valid: false, reason: `"${rootKey}" does not support nested keys` };
}
return { valid: true };
}
/**
* Get a nested value from an object using dot notation.
*
* @param obj - The object to access
* @param path - Dot-separated path (e.g., "featureFlags.someFlag")
* @returns The value at the path, or undefined if not found
*/
export function getNestedValue(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return undefined;
}
if (typeof current !== 'object') {
return undefined;
}
current = current[key];
}
return current;
}
/**
* Set a nested value in an object using dot notation.
* Creates intermediate objects as needed.
*
* @param obj - The object to modify (mutated in place)
* @param path - Dot-separated path (e.g., "featureFlags.someFlag")
* @param value - The value to set
*/
export function setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
const lastKey = keys[keys.length - 1];
current[lastKey] = value;
}
/**
* Delete a nested value from an object using dot notation.
*
* @param obj - The object to modify (mutated in place)
* @param path - Dot-separated path (e.g., "featureFlags.someFlag")
* @returns true if the key existed and was deleted, false otherwise
*/
export function deleteNestedValue(obj, path) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
return false;
}
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (lastKey in current) {
delete current[lastKey];
return true;
}
return false;
}
/**
* Coerce a string value to its appropriate type.
* - "true" / "false" -> boolean
* - Numeric strings -> number
* - Everything else -> string
*
* @param value - The string value to coerce
* @param forceString - If true, always return the value as a string
* @returns The coerced value
*/
export function coerceValue(value, forceString = false) {
if (forceString) {
return value;
}
// Boolean coercion
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
// Number coercion - must be a valid finite number
const num = Number(value);
if (!isNaN(num) && isFinite(num) && value.trim() !== '') {
return num;
}
return value;
}
/**
* Format a value for YAML-like display.
*
* @param value - The value to format
* @param indent - Current indentation level
* @returns Formatted string
*/
export function formatValueYaml(value, indent = 0) {
const indentStr = ' '.repeat(indent);
if (value === null || value === undefined) {
return 'null';
}
if (typeof value === 'boolean' || typeof value === 'number') {
return String(value);
}
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
if (value.length === 0) {
return '[]';
}
return value.map((item) => `${indentStr}- ${formatValueYaml(item, indent + 1)}`).join('\n');
}
if (typeof value === 'object') {
const entries = Object.entries(value);
if (entries.length === 0) {
return '{}';
}
return entries
.map(([key, val]) => {
const formattedVal = formatValueYaml(val, indent + 1);
if (typeof val === 'object' && val !== null && Object.keys(val).length > 0) {
return `${indentStr}${key}:\n${formattedVal}`;
}
return `${indentStr}${key}: ${formattedVal}`;
})
.join('\n');
}
return String(value);
}
/**
* Validate a configuration object against the schema.
*
* @param config - The configuration to validate
* @returns Validation result with success status and optional error message
*/
export function validateConfig(config) {
try {
GlobalConfigSchema.parse(config);
return { success: true };
}
catch (error) {
if (error instanceof z.ZodError) {
const zodError = error;
const messages = zodError.issues.map((e) => `${e.path.join('.')}: ${e.message}`);
return { success: false, error: messages.join('; ') };
}
return { success: false, error: 'Unknown validation error' };
}
}
//# sourceMappingURL=config-schema.js.map