hivecast-sdk/packages/cli/src/utils/package-store.ts

213 lines
7.0 KiB
TypeScript
Raw Normal View History

import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { validateManifestForMxCli } from './manifestValidator.js';
type JsonRecord = Record<string, unknown>;
export interface IResolvedManifest {
readonly packageName: string;
readonly manifest: JsonRecord;
}
const DEFAULT_GLOBAL_PACKAGES_ROOT = path.join(os.homedir(), '.matrix', 'packages');
const DEFAULT_GLOBAL_REGISTRY_ROOT = path.join(os.homedir(), '.matrix', 'registry');
export function resolvePackagesRoot(cwd: string, packagesDir?: string): string {
return path.resolve(cwd, packagesDir ?? DEFAULT_GLOBAL_PACKAGES_ROOT);
}
export function resolvePackagesNodeModulesRoot(cwd: string, packagesDir?: string): string {
return path.join(resolvePackagesRoot(cwd, packagesDir), 'node_modules');
}
export function resolveRegistryRoot(cwd: string, registryDir?: string): string {
return path.resolve(cwd, registryDir ?? DEFAULT_GLOBAL_REGISTRY_ROOT);
}
export function packageNameToPath(packageName: string): string {
if (packageName.includes('/')) {
return packageName;
}
return packageName.trim();
}
export function listInstalledPackageNames(nodeModulesRoot: string): string[] {
if (!fs.existsSync(nodeModulesRoot)) {
return [];
}
const entries = fs.readdirSync(nodeModulesRoot, { withFileTypes: true });
const names: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.')) continue;
if (entry.name.startsWith('@')) {
const scopeRoot = path.join(nodeModulesRoot, entry.name);
for (const scoped of fs.readdirSync(scopeRoot, { withFileTypes: true })) {
if (!scoped.isDirectory()) continue;
names.push(`${entry.name}/${scoped.name}`);
}
continue;
}
names.push(entry.name);
}
return names.sort((a, b) => a.localeCompare(b));
}
function readJsonFile(filePath: string): JsonRecord {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${filePath} must contain a JSON object`);
}
return parsed as JsonRecord;
}
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function readOptionalString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function readPackageJsonIfExists(packageDir: string): JsonRecord | null {
const packageJsonPath = path.join(packageDir, 'package.json');
return fs.existsSync(packageJsonPath) ? readJsonFile(packageJsonPath) : null;
}
function packageNameComparable(value: string): string {
const trimmed = value.trim();
if (trimmed.startsWith('@matrix/')) {
return trimmed.slice('@matrix/'.length);
}
if (trimmed.startsWith('@open-matrix/')) {
return trimmed.slice('@open-matrix/'.length);
}
if (trimmed.startsWith('@') && trimmed.includes('/')) {
return trimmed.slice(trimmed.indexOf('/') + 1);
}
return trimmed;
}
function assertMatrixAndPackageJsonAgree(
packageDir: string,
matrixManifest: JsonRecord,
packageJson: JsonRecord | null,
): void {
if (!packageJson) {
return;
}
const matrixName = readOptionalString(matrixManifest.name);
const packageName = readOptionalString(packageJson.name);
if (
matrixName
&& packageName
&& matrixName !== packageName
&& packageNameComparable(matrixName) !== packageNameComparable(packageName)
) {
throw new Error(
`MX_PACKAGE_IDENTITY_MISMATCH: matrix.json name (${matrixName}) `
+ `does not match package.json name (${packageName}) in ${packageDir}`,
);
}
const matrixVersion = readOptionalString(matrixManifest.version);
const packageVersion = readOptionalString(packageJson.version);
if (matrixVersion && packageVersion && matrixVersion !== packageVersion) {
throw new Error(
`MX_PACKAGE_VERSION_MISMATCH: matrix.json version (${matrixVersion}) `
+ `does not match package.json version (${packageVersion}) in ${packageDir}`,
);
}
}
export function readManifestFromPackageDir(packageDir: string): IResolvedManifest {
const matrixJsonPath = path.join(packageDir, 'matrix.json');
if (fs.existsSync(matrixJsonPath)) {
const matrixManifest = readJsonFile(matrixJsonPath);
const packageJson = readPackageJsonIfExists(packageDir);
assertMatrixAndPackageJsonAgree(packageDir, matrixManifest, packageJson);
const packageName = readOptionalString(matrixManifest.name);
if (!packageName) {
throw new Error(`matrix.json must contain a non-empty "name" field (${matrixJsonPath})`);
}
return {
packageName,
manifest: matrixManifest,
};
}
const packageJsonPath = path.join(packageDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Package manifest not found in ${packageDir} (expected matrix.json or package.json)`);
}
const packageJson = readJsonFile(packageJsonPath);
const packageName = typeof packageJson.name === 'string' ? packageJson.name.trim() : '';
if (!packageName) {
throw new Error(`package.json must contain a non-empty "name" field (${packageJsonPath})`);
}
const matrix = asRecord(packageJson.matrix) ?? {};
const manifest: JsonRecord = {
name: packageName,
version: packageJson.version,
runtime: matrix.runtime,
components: matrix.components,
permissions: matrix.permissions,
install: matrix.install,
};
return { packageName, manifest };
}
export function validateResolvedManifest(packageName: string, manifest: JsonRecord): void {
const validation = validateManifestForMxCli({
...manifest,
name: manifest.name ?? packageName,
});
if (!validation.valid) {
throw new Error(
`Manifest validation failed for ${packageName}: ${validation.errors.join('; ')}`
);
}
}
export function readPackageJson(packageDir: string): JsonRecord {
const packageJsonPath = path.join(packageDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`package.json not found in ${packageDir}`);
}
return readJsonFile(packageJsonPath);
}
export function readVersionFromPackageDir(packageDir: string): string {
const matrixJsonPath = path.join(packageDir, 'matrix.json');
const packageJson = readPackageJsonIfExists(packageDir);
if (fs.existsSync(matrixJsonPath)) {
const matrixManifest = readJsonFile(matrixJsonPath);
assertMatrixAndPackageJsonAgree(packageDir, matrixManifest, packageJson);
const matrixVersion = readOptionalString(matrixManifest.version);
const packageVersion = readOptionalString(packageJson?.version);
if (packageVersion.length > 0) {
return packageVersion;
}
if (matrixVersion.length > 0) {
return matrixVersion;
}
}
const version = readOptionalString((packageJson ?? readPackageJson(packageDir)).version);
if (!version) {
throw new Error(`Unable to resolve package version in ${packageDir}`);
}
return version;
}