- Add ProductManifestSchema with Zod validation for product identity - Support theme tokens, cosmos containers, feature flags, ports - Add loadProductManifest(), validateProductManifest(), resolveTheme() - Export all schemas and types from config package index
253 lines
6.6 KiB
TypeScript
253 lines
6.6 KiB
TypeScript
/**
|
|
* Product Manifest Specification
|
|
*
|
|
* Defines the JSON schema for product.json files that capture everything
|
|
* about a ByteLyst product — identity, theme, features, containers, flags, ports.
|
|
*
|
|
* @module @bytelyst/config/product-manifest
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
|
|
/**
|
|
* Platform identifiers
|
|
*/
|
|
export const PlatformSchema = z.enum(['web', 'ios', 'android', 'desktop', 'watch']);
|
|
|
|
/**
|
|
* Theme color token
|
|
*/
|
|
export const ColorTokenSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/, {
|
|
message: 'Color must be a hex value like #5AE68C',
|
|
});
|
|
|
|
/**
|
|
* Theme specification
|
|
*/
|
|
export const ThemeSchema = z.object({
|
|
primary: ColorTokenSchema,
|
|
secondary: ColorTokenSchema,
|
|
accent: ColorTokenSchema,
|
|
background: ColorTokenSchema,
|
|
surface: ColorTokenSchema,
|
|
text: ColorTokenSchema,
|
|
error: ColorTokenSchema,
|
|
warning: ColorTokenSchema,
|
|
success: ColorTokenSchema,
|
|
});
|
|
|
|
/**
|
|
* Cosmos container definition
|
|
*/
|
|
export const ContainerDefSchema = z.object({
|
|
name: z.string(),
|
|
partitionKey: z.string(),
|
|
ttlSeconds: z.number().optional(),
|
|
uniqueKeys: z.array(z.string()).optional(),
|
|
});
|
|
|
|
/**
|
|
* Feature flag default
|
|
*/
|
|
export const FeatureFlagSchema = z.object({
|
|
key: z.string(),
|
|
defaultValue: z.union([z.boolean(), z.string(), z.number()]),
|
|
description: z.string().optional(),
|
|
});
|
|
|
|
/**
|
|
* Port configuration
|
|
*/
|
|
export const PortConfigSchema = z.object({
|
|
service: z.number().optional(),
|
|
dashboard: z.number().optional(),
|
|
web: z.number().optional(),
|
|
});
|
|
|
|
/**
|
|
* Product manifest schema (Zod)
|
|
*
|
|
* Example product.json:
|
|
* ```json
|
|
* {
|
|
* "id": "lysnrai",
|
|
* "name": "LysnrAI",
|
|
* "displayName": "LysnrAI",
|
|
* "bundleId": "com.saravana.lysnrai",
|
|
* "domain": "lysnrai.app",
|
|
* "description": "Voice-to-text dictation platform",
|
|
* "platforms": ["web", "ios", "android", "desktop"],
|
|
* "theme": {
|
|
* "primary": "#5AE68C",
|
|
* "secondary": "#5A8CFF",
|
|
* "accent": "#2EE6D6",
|
|
* "background": "#06070A",
|
|
* "surface": "#121725",
|
|
* "text": "#EFF4FF",
|
|
* "error": "#FF6E6E",
|
|
* "warning": "#F59E0B",
|
|
* "success": "#34D399"
|
|
* },
|
|
* "features": {
|
|
* "voiceDictation": true,
|
|
* "realtimeTranscription": true,
|
|
* "keyboardExtension": true,
|
|
* "cloudSync": true
|
|
* },
|
|
* "cosmos": {
|
|
* "containers": [
|
|
* { "name": "transcripts", "partitionKey": "/userId" },
|
|
* { "name": "sessions", "partitionKey": "/userId" }
|
|
* ]
|
|
* },
|
|
* "flags": [
|
|
* { "key": "new-ui-enabled", "defaultValue": false },
|
|
* { "key": "max-audio-duration", "defaultValue": 300 }
|
|
* ],
|
|
* "ports": {
|
|
* "service": 4015,
|
|
* "dashboard": 3002
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
export const ProductManifestSchema = z.object({
|
|
// Identity (all required)
|
|
id: z.string().regex(/^[a-z][a-z0-9]*$/, {
|
|
message: 'Product ID must be lowercase alphanumeric, starting with letter',
|
|
}),
|
|
name: z.string().min(1).max(50),
|
|
displayName: z.string().min(1).max(50),
|
|
bundleId: z.string().regex(/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/, {
|
|
message: 'Bundle ID must be valid reverse-DNS format',
|
|
}),
|
|
domain: z.string().regex(/^[a-z0-9-]+\.[a-z]{2,}$/, {
|
|
message: 'Domain must be valid format like "product.app"',
|
|
}),
|
|
|
|
// Optional metadata
|
|
description: z.string().max(200).optional(),
|
|
platforms: z.array(PlatformSchema).default(['web']),
|
|
version: z.string().regex(/^\d+\.\d+\.\d+$/).optional(),
|
|
|
|
// Theming (optional, uses defaults if not specified)
|
|
theme: ThemeSchema.partial().optional(),
|
|
|
|
// Feature flags (optional)
|
|
features: z.record(
|
|
z.string(),
|
|
z.boolean().or(z.string()).or(z.number())
|
|
).optional(),
|
|
|
|
// Cosmos containers (optional)
|
|
cosmos: z.object({
|
|
containers: z.array(ContainerDefSchema).default([]),
|
|
}).optional(),
|
|
|
|
// Feature flags with defaults (optional)
|
|
flags: z.array(FeatureFlagSchema).default([]),
|
|
|
|
// Port configuration (optional)
|
|
ports: PortConfigSchema.optional(),
|
|
});
|
|
|
|
/**
|
|
* Extended manifest that allows additional unknown keys
|
|
* Use this when you need to access custom fields not in the schema
|
|
*/
|
|
export const ExtendedProductManifestSchema = ProductManifestSchema.passthrough();
|
|
|
|
/**
|
|
* Inferred TypeScript type for ProductManifest
|
|
*/
|
|
export type ProductManifest = z.infer<typeof ProductManifestSchema>;
|
|
export type Platform = z.infer<typeof PlatformSchema>;
|
|
export type Theme = z.infer<typeof ThemeSchema>;
|
|
export type ContainerDef = z.infer<typeof ContainerDefSchema>;
|
|
export type FeatureFlag = z.infer<typeof FeatureFlagSchema>;
|
|
export type PortConfig = z.infer<typeof PortConfigSchema>;
|
|
|
|
/**
|
|
* Default theme colors (ByteLyst brand palette)
|
|
*/
|
|
export const DEFAULT_THEME: Theme = {
|
|
primary: '#5AE68C',
|
|
secondary: '#5A8CFF',
|
|
accent: '#2EE6D6',
|
|
background: '#06070A',
|
|
surface: '#121725',
|
|
text: '#EFF4FF',
|
|
error: '#FF6E6E',
|
|
warning: '#F59E0B',
|
|
success: '#34D399',
|
|
};
|
|
|
|
/**
|
|
* Load and validate a product manifest from a file path
|
|
*
|
|
* @param path Path to product.json file
|
|
* @returns Validated ProductManifest
|
|
* @throws ZodError if validation fails
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const manifest = await loadProductManifest('./product.json');
|
|
* console.log(manifest.id); // 'lysnrai'
|
|
* ```
|
|
*/
|
|
export async function loadProductManifest(path: string): Promise<ProductManifest> {
|
|
const fs = await import('fs/promises');
|
|
const content = await fs.readFile(path, 'utf-8');
|
|
const json = JSON.parse(content);
|
|
return ProductManifestSchema.parse(json);
|
|
}
|
|
|
|
/**
|
|
* Synchronous version of loadProductManifest (for startup use)
|
|
*
|
|
* @param path Path to product.json file
|
|
* @returns Validated ProductManifest
|
|
* @throws ZodError if validation fails
|
|
*/
|
|
export function loadProductManifestSync(path: string): ProductManifest {
|
|
const fs = require('fs');
|
|
const content = fs.readFileSync(path, 'utf-8');
|
|
const json = JSON.parse(content);
|
|
return ProductManifestSchema.parse(json);
|
|
}
|
|
|
|
/**
|
|
* Merge manifest theme with defaults
|
|
*
|
|
* @param manifest Product manifest
|
|
* @returns Complete theme with defaults filled in
|
|
*/
|
|
export function resolveTheme(manifest: ProductManifest): Theme {
|
|
return {
|
|
...DEFAULT_THEME,
|
|
...manifest.theme,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate a product manifest object without loading from file
|
|
*
|
|
* @param json Parsed JSON object
|
|
* @returns Validated ProductManifest
|
|
* @throws ZodError if validation fails
|
|
*/
|
|
export function validateProductManifest(json: unknown): ProductManifest {
|
|
return ProductManifestSchema.parse(json);
|
|
}
|
|
|
|
/**
|
|
* Safe validation that returns null on failure
|
|
*
|
|
* @param json Parsed JSON object
|
|
* @returns Validated ProductManifest or null
|
|
*/
|
|
export function safeValidateProductManifest(json: unknown): ProductManifest | null {
|
|
const result = ProductManifestSchema.safeParse(json);
|
|
return result.success ? result.data : null;
|
|
}
|