Files
hyperframes/packages/cli/src/templates/remote.ts
T
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

69 lines
2.7 KiB
TypeScript

// Compat shim — fetchRemoteTemplate delegates to the registry resolver +
// installer (packages/cli/src/registry/). Kept so init.ts and external imports
// that reference this path keep working. Deletable once init.ts is fully
// ported to call the resolver directly.
import { existsSync } from "node:fs";
import { join } from "node:path";
import { installItem, listRegistryItems, loadAllItems } from "../registry/index.js";
import { resolveItemWithDependencies } from "../registry/resolver.js";
import { gateRegistryItemsCompatibility } from "../registry/compatibility.js";
// Re-exported for the existing remote.test.ts regression guard. These paths
// describe the repo layout under the default registry URL; updating them in
// sync with any future move prevents silent breakage of installed CLIs.
export const TEMPLATES_DIR = "registry/examples";
export const MANIFEST_FILENAME = "templates.json";
export interface RemoteTemplateInfo {
id: string;
label: string;
hint: string;
bundled: boolean;
}
/**
* List available remote templates — kept for backwards compat with external
* imports. Internally, `resolveTemplateList` in generators.ts is what init.ts
* uses, and it goes through the registry resolver directly.
*/
export async function listRemoteTemplates(): Promise<RemoteTemplateInfo[]> {
const entries = await listRegistryItems({ type: "hyperframes:example" });
const items = await loadAllItems(entries);
return items.map((item) => ({
id: item.name,
label: item.title,
hint: item.description,
bundled: false,
}));
}
/**
* Download a template into destDir. Delegates to the registry installer.
*
* Resolves the template's transitive `registryDependencies` and installs them
* before the template itself, so a template that depends on other registry
* items gets a complete install rather than silently dropping its deps.
*
* Every resolved item is compatibility-gated up front (same gate as
* `hyperframes add`), so an incompatible template — or any of its deps —
* aborts before a single file is written.
*/
export async function fetchRemoteTemplate(templateId: string, destDir: string): Promise<void> {
const items = await resolveItemWithDependencies(templateId);
const warnings = gateRegistryItemsCompatibility(items);
for (const warning of warnings) {
process.stderr.write(`hyperframes:registry ${warning}\n`);
}
for (const item of items) {
await installItem(item, { destDir });
}
// Safety check — an item with no index.html isn't a valid example.
if (!existsSync(join(destDir, "index.html"))) {
throw new Error(
`Example "${templateId}" installed but missing index.html. The registry item may be malformed.`,
);
}
}