@fission-ai/openspec
Version:
AI-native system for spec-driven development
257 lines • 10.5 kB
JavaScript
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getGlobalDataDir } from '../global-config.js';
import { parseSchema, SchemaValidationError } from './schema.js';
/**
* Error thrown when loading a schema fails.
*/
export class SchemaLoadError extends Error {
schemaPath;
cause;
constructor(message, schemaPath, cause) {
super(message);
this.schemaPath = schemaPath;
this.cause = cause;
this.name = 'SchemaLoadError';
}
}
/**
* Gets the package's built-in schemas directory path.
* Uses import.meta.url to resolve relative to the current module.
*/
export function getPackageSchemasDir() {
const currentFile = fileURLToPath(import.meta.url);
// Navigate from dist/core/artifact-graph/ to package root's schemas/
return path.join(path.dirname(currentFile), '..', '..', '..', 'schemas');
}
/**
* Gets the user's schema override directory path.
*/
export function getUserSchemasDir() {
return path.join(getGlobalDataDir(), 'schemas');
}
/**
* Gets the project-local schemas directory path.
* @param projectRoot - The project root directory
* @returns The path to the project's schemas directory
*/
export function getProjectSchemasDir(projectRoot) {
return path.join(projectRoot, 'openspec', 'schemas');
}
/**
* Resolves a schema name to its directory path.
*
* Resolution order (when projectRoot is provided):
* 1. Project-local: <projectRoot>/openspec/schemas/<name>/schema.yaml
* 2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
* 3. Package built-in: <package>/schemas/<name>/schema.yaml
*
* When projectRoot is not provided, only user override and package built-in are checked
* (backward compatible behavior).
*
* @param name - Schema name (e.g., "spec-driven")
* @param projectRoot - Optional project root directory for project-local schema resolution
* @returns The path to the schema directory, or null if not found
*/
export function getSchemaDir(name, projectRoot) {
// 1. Check project-local directory (if projectRoot provided)
if (projectRoot) {
const projectDir = path.join(getProjectSchemasDir(projectRoot), name);
const projectSchemaPath = path.join(projectDir, 'schema.yaml');
if (fs.existsSync(projectSchemaPath)) {
return projectDir;
}
}
// 2. Check user override directory
const userDir = path.join(getUserSchemasDir(), name);
const userSchemaPath = path.join(userDir, 'schema.yaml');
if (fs.existsSync(userSchemaPath)) {
return userDir;
}
// 3. Check package built-in directory
const packageDir = path.join(getPackageSchemasDir(), name);
const packageSchemaPath = path.join(packageDir, 'schema.yaml');
if (fs.existsSync(packageSchemaPath)) {
return packageDir;
}
return null;
}
/**
* Resolves a schema name to a SchemaYaml object.
*
* Resolution order (when projectRoot is provided):
* 1. Project-local: <projectRoot>/openspec/schemas/<name>/schema.yaml
* 2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
* 3. Package built-in: <package>/schemas/<name>/schema.yaml
*
* When projectRoot is not provided, only user override and package built-in are checked
* (backward compatible behavior).
*
* @param name - Schema name (e.g., "spec-driven")
* @param projectRoot - Optional project root directory for project-local schema resolution
* @returns The resolved schema object
* @throws Error if schema is not found in any location
*/
export function resolveSchema(name, projectRoot) {
// Normalize name (remove .yaml extension if provided)
const normalizedName = name.replace(/\.ya?ml$/, '');
const schemaDir = getSchemaDir(normalizedName, projectRoot);
if (!schemaDir) {
const availableSchemas = listSchemas(projectRoot);
throw new Error(`Schema '${normalizedName}' not found. Available schemas: ${availableSchemas.join(', ')}`);
}
const schemaPath = path.join(schemaDir, 'schema.yaml');
// Load and parse the schema
let content;
try {
content = fs.readFileSync(schemaPath, 'utf-8');
}
catch (err) {
const ioError = err instanceof Error ? err : new Error(String(err));
throw new SchemaLoadError(`Failed to read schema at '${schemaPath}': ${ioError.message}`, schemaPath, ioError);
}
try {
return parseSchema(content);
}
catch (err) {
if (err instanceof SchemaValidationError) {
throw new SchemaLoadError(`Invalid schema at '${schemaPath}': ${err.message}`, schemaPath, err);
}
const parseError = err instanceof Error ? err : new Error(String(err));
throw new SchemaLoadError(`Failed to parse schema at '${schemaPath}': ${parseError.message}`, schemaPath, parseError);
}
}
/**
* Lists all available schema names.
* Combines project-local, user override, and package built-in schemas.
*
* @param projectRoot - Optional project root directory for project-local schema resolution
*/
export function listSchemas(projectRoot) {
const schemas = new Set();
// Add package built-in schemas
const packageDir = getPackageSchemasDir();
if (fs.existsSync(packageDir)) {
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
schemas.add(entry.name);
}
}
}
}
// Add user override schemas (may override package schemas)
const userDir = getUserSchemasDir();
if (fs.existsSync(userDir)) {
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
schemas.add(entry.name);
}
}
}
}
// Add project-local schemas (if projectRoot provided)
if (projectRoot) {
const projectDir = getProjectSchemasDir(projectRoot);
if (fs.existsSync(projectDir)) {
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
schemas.add(entry.name);
}
}
}
}
}
return Array.from(schemas).sort();
}
/**
* Lists all available schemas with their descriptions and artifact lists.
* Useful for agent skills to present schema selection to users.
*
* @param projectRoot - Optional project root directory for project-local schema resolution
*/
export function listSchemasWithInfo(projectRoot) {
const schemas = [];
const seenNames = new Set();
// Add project-local schemas first (highest priority, if projectRoot provided)
if (projectRoot) {
const projectDir = getProjectSchemasDir(projectRoot);
if (fs.existsSync(projectDir)) {
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
try {
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
schemas.push({
name: entry.name,
description: schema.description || '',
artifacts: schema.artifacts.map((a) => a.id),
source: 'project',
});
seenNames.add(entry.name);
}
catch {
// Skip invalid schemas
}
}
}
}
}
}
// Add user override schemas (if not overridden by project)
const userDir = getUserSchemasDir();
if (fs.existsSync(userDir)) {
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
if (entry.isDirectory() && !seenNames.has(entry.name)) {
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
try {
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
schemas.push({
name: entry.name,
description: schema.description || '',
artifacts: schema.artifacts.map((a) => a.id),
source: 'user',
});
seenNames.add(entry.name);
}
catch {
// Skip invalid schemas
}
}
}
}
}
// Add package built-in schemas (if not overridden by project or user)
const packageDir = getPackageSchemasDir();
if (fs.existsSync(packageDir)) {
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
if (entry.isDirectory() && !seenNames.has(entry.name)) {
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
try {
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
schemas.push({
name: entry.name,
description: schema.description || '',
artifacts: schema.artifacts.map((a) => a.id),
source: 'package',
});
}
catch {
// Skip invalid schemas
}
}
}
}
}
return schemas.sort((a, b) => a.name.localeCompare(b.name));
}
//# sourceMappingURL=resolver.js.map