feat(config): implement P0-1 - Product manifest specification (product.json schema)
- 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
This commit is contained in:
parent
89458d7f75
commit
7f5ff4c790
@ -13,3 +13,24 @@ export {
|
||||
type SecretMapping,
|
||||
type SecretsProviderType,
|
||||
} from './keyvault.js';
|
||||
export {
|
||||
ProductManifestSchema,
|
||||
PlatformSchema,
|
||||
ThemeSchema,
|
||||
ContainerDefSchema,
|
||||
FeatureFlagSchema,
|
||||
PortConfigSchema,
|
||||
ExtendedProductManifestSchema,
|
||||
DEFAULT_THEME,
|
||||
loadProductManifest,
|
||||
loadProductManifestSync,
|
||||
resolveTheme,
|
||||
validateProductManifest,
|
||||
safeValidateProductManifest,
|
||||
type ProductManifest,
|
||||
type Platform,
|
||||
type Theme,
|
||||
type ContainerDef,
|
||||
type FeatureFlag,
|
||||
type PortConfig,
|
||||
} from './product-manifest.js';
|
||||
|
||||
252
packages/config/src/product-manifest.ts
Normal file
252
packages/config/src/product-manifest.ts
Normal file
@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user