Files
heygen-com__hyperframes/packages/cli/src/registry/compatibility.ts
James Russo 8eac7e1cda fix(cli): resolve and install transitive registry dependencies (#1396)
* fix(cli): resolve and install transitive registry dependencies

`hyperframes add`, `hyperframes new` (fetchRemoteTemplate), and the studio
"add block" path each resolved a single registry item and silently dropped
any `registryDependencies` it declared.

Add `resolveItemWithDependencies` (DFS topological sort, cycle detection,
missing-dependency errors, and dedup of shared/diamond deps) and route all
three install paths through it so dependencies are installed before the item
that needs them. `resolveItem` becomes a thin guard that throws on dep-bearing
items, so no future caller can silently reintroduce the drop. `runAdd` now
returns the ordered `installed` list and compatibility-gates every dependency
before any write.

Reworks the stale PR #414 onto current main and addresses its review feedback:
fetchRemoteTemplate installs deps, no out-of-scope files, dead null-checks
dropped, diamond test added, and the deliberate serial-fetch tradeoff is noted.

Co-authored-by: Rakibul Islam <40rakib70@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cli): make getItem async so missing-dep surfaces as rejection

Addresses review nit on #1396: getItem was typed Promise<RegistryItem> but
threw synchronously on a missing dependency. Marking it async keeps the
control flow consistent with the return type — the throw now becomes a
rejection. The body has no await, so the item cache is still populated
synchronously on first request and dedup is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): compatibility-gate transitive deps in all install paths

Addresses Via's review on #1396: `assertCompatibleOrThrow` only ran inside
`runAdd`, so `fetchRemoteTemplate` (hyperframes new) and the Studio
"add block" action installed resolved items — now including transitive
dependencies — with no minCliVersion enforcement or deprecation warnings. A
pre-existing single-item asymmetry that this PR's dep loops amplify across N
items.

- Add shared `gateRegistryItemsCompatibility` + `RegistryCompatibilityError`
  to compatibility.ts; all three install paths now gate the full resolved set
  before any write. `runAdd` keeps its AddError mapping by wrapping the shared
  gate.
- Surface deprecation warnings from the template/studio paths to stderr.
- Extract the studio viewport rewrite into `rewriteWrittenToHostViewport`
  (also drops redundant dynamic node:fs imports) and document that it
  intentionally rewrites dep-shipped .html too (Via item 3).
- Unit-test the shared gate directly (no fetch/cache flakiness): compatible
  set, accumulated deprecation warnings, and throw-on-incompatible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Rakibul Islam <40rakib70@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 17:18:15 -07:00

79 lines
2.4 KiB
TypeScript

import type { RegistryItem } from "@hyperframes/core";
import { compareVersions } from "compare-versions";
import { VERSION } from "../version.js";
export interface RegistryCompatibilityResult {
warnings: string[];
error?: string;
}
const DEV_VERSION = "0.0.0-dev";
export function checkRegistryItemCompatibility(
item: RegistryItem,
currentCliVersion = VERSION,
): RegistryCompatibilityResult {
const warnings: string[] = [];
if (item.deprecated) {
warnings.push(`Registry item "${item.name}" is deprecated: ${item.deprecated}`);
}
const minCliVersion = item.minCliVersion?.trim();
if (!minCliVersion || currentCliVersion === DEV_VERSION) {
return { warnings };
}
try {
if (compareVersions(currentCliVersion, minCliVersion) >= 0) {
return { warnings };
}
} catch {
return {
warnings,
error: `Registry item "${item.name}" declares invalid minCliVersion "${minCliVersion}".`,
};
}
return {
warnings,
error:
`Registry item "${item.name}" requires hyperframes >= ${minCliVersion} ` +
`(current: ${currentCliVersion}). Run \`npx hyperframes@latest add ${item.name}\` ` +
"or upgrade your installed hyperframes CLI.",
};
}
/** Thrown by `gateRegistryItemsCompatibility` when an item requires a newer CLI. */
export class RegistryCompatibilityError extends Error {
constructor(message: string) {
super(message);
this.name = "RegistryCompatibilityError";
}
}
/**
* Compatibility-gate a set of resolved items (e.g. an item plus its transitive
* `registryDependencies`) before any of them are installed. Throws a
* `RegistryCompatibilityError` on the first item that requires a newer CLI, so
* a partial install never happens; returns the accumulated (non-fatal)
* deprecation warnings from every item.
*
* Every install path — `add`, template fetch, and the Studio "add block"
* action — funnels through this so a dependency that ships `minCliVersion` is
* rejected uniformly, not just by `hyperframes add`.
*/
export function gateRegistryItemsCompatibility(
items: RegistryItem[],
currentCliVersion = VERSION,
): string[] {
const warnings: string[] = [];
for (const item of items) {
const result = checkRegistryItemCompatibility(item, currentCliVersion);
if (result.error) {
throw new RegistryCompatibilityError(result.error);
}
warnings.push(...result.warnings);
}
return warnings;
}