mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(plugins): ship portable agent bundles and installation guides (#4608)
* feat(plugins): ship portable agent bundles and installation guides * fix(plugins): align new manifests with current release * test(plugins): split caption fixture setup and cleanup * test(plugins): pass render fixture path as an argument
This commit is contained in:
@@ -6,10 +6,17 @@
|
||||
"url": "https://hyperframes.heygen.com"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "hyperframes",
|
||||
"source": "./",
|
||||
"description": "Recommended: all published HyperFrames skills in one versioned bundle. Create and render videos from HTML; load workflows on demand without installing newer standalone skills.",
|
||||
"homepage": "https://hyperframes.heygen.com",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"name": "core-skills",
|
||||
"source": "./",
|
||||
"description": "The HyperFrames core set \u2014 install this one: the /hyperframes router, the hyperframes-* domain skills, and media-use. The router installs each creation workflow on demand. The full bundle (all skills at once) is `hyperframes`.",
|
||||
"description": "Legacy core-only installation. For release-managed bundled workflows, install hyperframes@hyperframes and then uninstall core-skills@hyperframes.",
|
||||
"homepage": "https://hyperframes.heygen.com",
|
||||
"license": "Apache-2.0",
|
||||
"skills": [
|
||||
@@ -24,13 +31,7 @@
|
||||
"./skills/hyperframes-studio",
|
||||
"./skills/media-use"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hyperframes",
|
||||
"source": "./",
|
||||
"description": "All HyperFrames skills in one static bundle. Most setups want `core-skills` instead \u2014 the router installs creation workflows on demand. HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
|
||||
"homepage": "https://hyperframes.heygen.com",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "HyperFrames video creation skills, versioned and updated as one plugin."
|
||||
}
|
||||
|
||||
@@ -134,6 +134,16 @@ jobs:
|
||||
- "package.json"
|
||||
- ".github/workflows/ci.yml"
|
||||
codex_plugin:
|
||||
- "skills-manifest.json"
|
||||
- "packages/cli/package.json"
|
||||
- "plugin.json"
|
||||
- "gemini-extension.json"
|
||||
- ".claude-plugin/**"
|
||||
- ".cursor-plugin/**"
|
||||
- "scripts/package-agent-plugin*"
|
||||
- "scripts/agent-plugin-captions.test.mjs"
|
||||
- "scripts/set-version*"
|
||||
- "LICENSE"
|
||||
- ".codex-plugin/**"
|
||||
- "assets/**"
|
||||
- "skills/**"
|
||||
@@ -643,6 +653,12 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Test portable plugin packaging
|
||||
run: node --test scripts/package-agent-plugin.test.mjs
|
||||
- name: Build portable agent plugin
|
||||
run: node scripts/package-agent-plugin.mjs
|
||||
- name: Exercise captions from the extracted portable bundle
|
||||
run: node --test scripts/agent-plugin-captions.test.mjs
|
||||
- name: Build upload-ready Codex plugin
|
||||
run: node scripts/package-codex-plugin.mjs
|
||||
|
||||
|
||||
@@ -214,3 +214,13 @@ jobs:
|
||||
fi
|
||||
gh release create "v${VERSION}" "${FLAGS[@]}"
|
||||
fi
|
||||
|
||||
- name: Build portable agent plugin
|
||||
run: node scripts/package-agent-plugin.mjs
|
||||
|
||||
- name: Attach portable plugin to release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: gh release upload "v${VERSION}" dist/hyperframes-agent-plugin.zip dist/hyperframes-agent-plugin.sha256 dist/hyperframes-agent-plugin.json --repo "$REPO" --clobber
|
||||
|
||||
@@ -35,7 +35,18 @@ HyperFrames is an open-source framework for turning HTML, CSS, media, and seekab
|
||||
|
||||
### With an AI coding agent
|
||||
|
||||
Install the HyperFrames skills, then describe the video you want:
|
||||
For Claude Code, install the versioned plugin:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add heygen-com/hyperframes
|
||||
claude plugin install hyperframes@hyperframes
|
||||
```
|
||||
|
||||
Enable auto-update for the **hyperframes** marketplace in `/plugin` → **Marketplaces**,
|
||||
then use `/hyperframes:hyperframes`. See the [plugin guide](docs/guides/plugins.mdx)
|
||||
for Copilot, VS Code, Cursor, Gemini CLI, updates, and migration.
|
||||
|
||||
For standalone skills (including OpenCode), use:
|
||||
|
||||
```bash
|
||||
npx skills add heygen-com/hyperframes
|
||||
@@ -55,9 +66,15 @@ The skills teach agents the HyperFrames production loop: plan the video, write v
|
||||
|
||||
HyperFrames ships 21 skills agents load on demand. Read `/hyperframes` first — it's the router and capability map; it picks a workflow for any "make me a…" request — video, deck, or composition port — and points to the domain skills below.
|
||||
|
||||
Default to the **core set** — the router installs each creation workflow on demand. `npx hyperframes skills update` installs exactly that from anywhere; the interactive picker (`npx skills add heygen-com/hyperframes`) lists it as the "Core Skills" group, nothing pre-selected. The picker is interactive-only — a non-interactive or agent run without `--skill` installs all 21. Use `npx skills add heygen-com/hyperframes --all` to install all 20 deliberately (skips the picker), or `npx skills add heygen-com/hyperframes --skill <name>` for just one (bare name, no leading `/`).
|
||||
For standalone installation, default to the **core set** — the router installs each creation workflow on demand. `npx hyperframes skills update` installs exactly that from anywhere; the interactive picker (`npx skills add heygen-com/hyperframes`) lists it as the "Core Skills" group, nothing pre-selected. The picker is interactive-only — a non-interactive or agent run without `--skill` installs all 21. Use `npx skills add heygen-com/hyperframes --all` to install the complete published set deliberately (skips the picker), or `npx skills add heygen-com/hyperframes --skill <name>` for just one (bare name, no leading `/`).
|
||||
|
||||
Installs stay lean after that: `npx hyperframes init` keeps the **core set** fresh (the router, the `hyperframes-*` domain skills, and `media-use` — plus whatever is already installed; `/figma` stays on demand) and never expands a partial install; the creation workflows install **on demand** — the router runs `npx hyperframes skills update <workflow>` before entering one. Nothing re-pulls the full set behind your back.
|
||||
Standalone installs stay lean after that: `npx hyperframes init` keeps the **core set** fresh (the router, the `hyperframes-*` domain skills, and `media-use` — plus whatever is already installed; `/figma` stays on demand) and never expands a partial install; the creation workflows install **on demand** — the router runs `npx hyperframes skills update <workflow>` before entering one. Nothing re-pulls the full set behind your back.
|
||||
|
||||
### Plugin packages
|
||||
|
||||
Plugins bundle the full skill catalog and use their agent's update manager.
|
||||
`bun run package:agent-plugin` builds the committed portable ZIP, source metadata,
|
||||
and SHA-256 checksum. See the [maintainer checklist](docs/contributing/agent-plugins.mdx).
|
||||
|
||||
### Upload to Codex
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: "Release agent plugins"
|
||||
description: "Build, verify, and publish HyperFrames plugin packages from one committed release."
|
||||
---
|
||||
|
||||
`skills/` is the canonical published skill source. Claude, Agent Plugins 1.0,
|
||||
Cursor, Codex, and Gemini manifests share the CLI release version. Keep vendor
|
||||
manifests for existing clients while the portable format is adopted.
|
||||
|
||||
## Build a committed release
|
||||
|
||||
Use the normal reviewed [release process](/contributing/release-channels).
|
||||
`set-version` updates all five plugin manifests. Commit the release before
|
||||
packaging; package builders intentionally exclude uncommitted changes.
|
||||
|
||||
```bash
|
||||
bun run package:agent-plugin
|
||||
bun run package:codex-plugin
|
||||
```
|
||||
|
||||
The portable builder writes these files under `dist/`:
|
||||
|
||||
- `hyperframes-agent-plugin.zip`: a `hyperframes/` root with manifests, skills,
|
||||
the skill catalog, brand assets, and license.
|
||||
- `hyperframes-agent-plugin.json`: source commit, version, skill inventory,
|
||||
archive size, and SHA-256.
|
||||
- `hyperframes-agent-plugin.sha256`: the archive checksum.
|
||||
|
||||
The publish workflow attaches the portable files to the GitHub release after npm
|
||||
publication. The Codex ZIP remains a separate submission artifact. Building it
|
||||
does not update the OpenAI directory listing.
|
||||
|
||||
## Verify before advertising support
|
||||
|
||||
1. Run packaging tests, skill validation, skill manifest checks, and manifest
|
||||
validation. Check that no contributor-only skills, LFS pointers needed at
|
||||
runtime, or dependencies on unbundled repository files entered the payload.
|
||||
2. In isolated profiles, install the full bundle and check its skill inventory.
|
||||
Exercise both Claude's namespaced router and each client's native discovery.
|
||||
3. Upgrade from release N to N+1 through the actual distribution source. Confirm
|
||||
the new version after reload; verify a pinned installation stays on N.
|
||||
A local-directory reload is not proof of marketplace auto-update.
|
||||
4. Run a local title-card workflow. Verify `init` does not refresh global skills,
|
||||
the CLI version matches the plugin, and plugin files remain unchanged. Check
|
||||
existing project dependencies without rewriting their lockfiles.
|
||||
5. Test migration from `core-skills` and duplicate standalone skills, then
|
||||
uninstall. Preserve unrelated skills and user edits.
|
||||
6. Record tested client versions, operating systems, source commit, and results
|
||||
in the PR/release notes. Mark unavailable clients as unverified.
|
||||
|
||||
The Claude marketplace keeps the old `core-skills` install identity for migration;
|
||||
new users should install `hyperframes@hyperframes`. Do not remove the old entry
|
||||
without testing migration on supported Claude versions.
|
||||
|
||||
## Distribution status and rollback
|
||||
|
||||
Track self-hosted repository installation, release artifact publication, native
|
||||
client verification, and public marketplace approval separately. Cursor public
|
||||
updates require review. OpenAI directory submissions follow their own process.
|
||||
Never describe a successful manifest check as a verified native installation.
|
||||
|
||||
For rollback, reinstall the prior tested release tag or archive and disable
|
||||
updates temporarily. Do not reuse a version number for changed plugin contents.
|
||||
If a public marketplace has a separate published revision, roll that back through
|
||||
its own process too.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Install agent plugins](/guides/plugins)
|
||||
- [Release channels](/contributing/release-channels)
|
||||
- [Test local changes](/contributing/testing-local-changes)
|
||||
@@ -948,6 +948,7 @@
|
||||
"group": "Agent setup",
|
||||
"pages": [
|
||||
"guides/authentication",
|
||||
"guides/plugins",
|
||||
"guides/skills",
|
||||
"guides/webmcp"
|
||||
]
|
||||
@@ -958,6 +959,7 @@
|
||||
"contributing",
|
||||
"contributing/catalog",
|
||||
"contributing/release-channels",
|
||||
"contributing/agent-plugins",
|
||||
"contributing/changelog-process",
|
||||
"contributing/testing-local-changes",
|
||||
"contributing/canary-rollouts",
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "Install the HyperFrames plugin"
|
||||
sidebarTitle: "Agent plugins"
|
||||
description: "Install video creation skills as one versioned plugin and let your agent manage updates."
|
||||
---
|
||||
|
||||
The HyperFrames plugin gives your coding agent the complete set of video workflows:
|
||||
product promos, explainers, captions, motion graphics, and more. Skills load when
|
||||
needed; you do not have to install each workflow separately.
|
||||
|
||||
Choose one installation method per agent. Plugins receive updates through the
|
||||
agent's plugin manager. [Standalone skills](/guides/skills) remain available for
|
||||
OpenCode and other agents, or when you prefer the smaller core-only install.
|
||||
|
||||
| Agent | Installation route |
|
||||
| --- | --- |
|
||||
| Claude Code | HyperFrames' GitHub marketplace |
|
||||
| Copilot CLI | HyperFrames' GitHub marketplace |
|
||||
| VS Code / Copilot | Add the GitHub marketplace in the Extensions view |
|
||||
| Cursor | Import through a team marketplace, or load a local plugin |
|
||||
| Gemini CLI | Install the HyperFrames extension from GitHub |
|
||||
| Codex | Use your existing HyperFrames plugin; directory updates are published separately |
|
||||
| OpenCode | Use standalone skills |
|
||||
|
||||
You need Node.js 22+ and npm for the local CLI, plus FFmpeg for rendering.
|
||||
Install on each machine you use; a plugin does not synchronize your machines.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Run in your terminal:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add heygen-com/hyperframes
|
||||
claude plugin install hyperframes@hyperframes
|
||||
claude plugin details hyperframes
|
||||
```
|
||||
|
||||
The inventory should include `hyperframes`, `general-video`, and `media-use`.
|
||||
Start a new session or run `/reload-plugins`, then try:
|
||||
|
||||
```text
|
||||
Using /hyperframes:hyperframes, make a 5-second title card that says "Hello world".
|
||||
```
|
||||
|
||||
**Enable automatic updates:** run `/plugin`, open **Marketplaces**, select
|
||||
**hyperframes**, and choose **Enable auto-update**. Third-party marketplaces do
|
||||
not enable this by default. To update manually:
|
||||
|
||||
```bash
|
||||
claude plugin update hyperframes@hyperframes
|
||||
```
|
||||
|
||||
Reload plugins after updating. To remove the plugin:
|
||||
|
||||
```bash
|
||||
claude plugin uninstall hyperframes@hyperframes
|
||||
```
|
||||
|
||||
See [Claude's plugin guide](https://code.claude.com/docs/en/plugins/install)
|
||||
for project-wide installation and managed update policies.
|
||||
|
||||
## GitHub Copilot CLI
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add heygen-com/hyperframes
|
||||
copilot plugin install hyperframes@hyperframes
|
||||
copilot plugin list
|
||||
```
|
||||
|
||||
Start a fresh session and ask Copilot to use the HyperFrames skill to make the
|
||||
same title card. Check `/skills list` if the skill is not visible.
|
||||
|
||||
```bash
|
||||
copilot plugin update hyperframes
|
||||
copilot plugin uninstall hyperframes
|
||||
```
|
||||
|
||||
These are explicit updates; do not assume Copilot CLI shares VS Code's update
|
||||
schedule. See [Copilot's plugin guide](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing).
|
||||
|
||||
## VS Code
|
||||
|
||||
Open the Extensions view and search `@agentPlugins`. Add
|
||||
`heygen-com/hyperframes` as a marketplace using the plugin marketplace command,
|
||||
then install **hyperframes** and verify its skills in Agent Customizations.
|
||||
Agent plugins must be enabled in VS Code settings.
|
||||
|
||||
VS Code checks for updates every 24 hours when `extensions.autoUpdate` is enabled.
|
||||
Use **Extensions: Check for Extension Updates** for a manual check. Disable or
|
||||
uninstall the plugin from the Extensions view. See [VS Code's plugin guide](https://code.visualstudio.com/docs/agent-customization/agent-plugins)
|
||||
for the current UI and workspace recommendations.
|
||||
|
||||
## Cursor
|
||||
|
||||
For a team marketplace, import `heygen-com/hyperframes` in **Dashboard → Plugins &
|
||||
MCPs** and add the HyperFrames plugin. Teammates install it from **Customize**.
|
||||
GitHub marketplace auto-refresh requires the Cursor GitHub App and the marketplace's
|
||||
**Enable Auto Refresh** setting.
|
||||
|
||||
For a local install, copy an extracted HyperFrames plugin folder to
|
||||
`~/.cursor/plugins/local/hyperframes`, then reload Cursor and check **Customize**.
|
||||
The folder must contain `plugin.json` and `skills/`. Local copies require manual
|
||||
replacement to update; they do not subscribe to GitHub changes. Enterprise policy
|
||||
may disable local plugin imports. Remove the local folder to uninstall that copy.
|
||||
|
||||
This repository's manifests are not proof of a public Cursor marketplace listing.
|
||||
Public listings and their updates go through Cursor review. See [Cursor's plugin
|
||||
guide](https://cursor.com/docs/plugins) for marketplace and local-install controls.
|
||||
|
||||
## Gemini CLI
|
||||
|
||||
```bash
|
||||
gemini extensions install https://github.com/heygen-com/hyperframes --auto-update
|
||||
gemini extensions list
|
||||
```
|
||||
|
||||
Restart Gemini CLI and ask it to use the HyperFrames skill for the title card.
|
||||
To manage the extension explicitly:
|
||||
|
||||
```bash
|
||||
gemini extensions update hyperframes
|
||||
gemini extensions uninstall hyperframes
|
||||
```
|
||||
|
||||
To stay on a specific release, install with `--ref <release-tag>` instead of
|
||||
`--auto-update`. Choose a release containing `gemini-extension.json`.
|
||||
See [Gemini's extension guide](https://geminicli.com/docs/extensions/reference/).
|
||||
|
||||
The embedded-captions workflow additionally needs project-local Sharp, Puppeteer
|
||||
with Chromium, and GSAP. Its skill includes the versioned installation command;
|
||||
keep those dependencies and their lockfile in the caption project. The plugin
|
||||
itself remains read-only and does not include browsers or model weights.
|
||||
|
||||
## Versions and updates
|
||||
|
||||
The plugin bundles workflows from one source snapshot and runs the CLI version
|
||||
recorded in its manifest. Your agent should use its bundled plugin launcher;
|
||||
it should not run `npx hyperframes skills update` during a plugin workflow.
|
||||
Existing project dependencies, downloaded media, and external services have their
|
||||
own versions and are not frozen by the plugin.
|
||||
|
||||
Update behavior depends on the client and source ref. For example,
|
||||
[Cursor GitHub Auto Refresh](https://cursor.com/docs/plugins#keep-plugins-up-to-date)
|
||||
follows pushes to the tracked branch, so skill content can change without a new
|
||||
CLI version. The manifest pins the CLI version, not the installed skill commit.
|
||||
For a repeatable installation, keep a release tag or an extracted release archive
|
||||
and disable automatic updates. Install that same release on each machine.
|
||||
Git-based installs may download the full source repository. Release ZIPs contain
|
||||
only the plugin payload, but loading an extracted ZIP locally requires manual updates.
|
||||
Only releases with a `hyperframes-agent-plugin.zip` asset provide that portable ZIP.
|
||||
|
||||
## Move from standalone skills or core-skills
|
||||
|
||||
Install the full plugin and verify its inventory first. In Claude, an older
|
||||
`core-skills@hyperframes` install can then be removed:
|
||||
|
||||
```bash
|
||||
claude plugin uninstall core-skills@hyperframes
|
||||
```
|
||||
|
||||
Use your standalone skill manager to remove only the old HyperFrames skills for
|
||||
that agent. Inspect the locations first: other agents may share a global skills
|
||||
store, and locally edited skills should be backed up. Do not delete an entire
|
||||
`.agents` or `.claude` directory. Restart the agent and confirm it selects the
|
||||
plugin copy rather than a duplicate standalone skill.
|
||||
|
||||
## If a workflow is missing or stale
|
||||
|
||||
1. Check the agent's installed plugin inventory and version.
|
||||
2. Update using that agent's plugin manager, then reload or restart it.
|
||||
3. Check whether an old standalone installation is also being loaded.
|
||||
4. For a pinned release, verify the workflow exists in that release. Do not mix
|
||||
skills from a newer release into the plugin folder.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Make your first video](/quickstart)
|
||||
- [Install standalone skills](/guides/skills)
|
||||
- [Plugin release checklist](/contributing/agent-plugins)
|
||||
@@ -18,6 +18,10 @@ The terminal output is a real run of the command. Afterwards the router reads a
|
||||
request and chooses the workflow for it, instead of the agent guessing at the
|
||||
framework's rules.
|
||||
|
||||
Prefer a [versioned plugin](/guides/plugins) when your agent supports one. This
|
||||
page covers standalone installs, including OpenCode. Plugin users should update
|
||||
through their agent's plugin manager instead of running the commands below.
|
||||
|
||||
## Install for a person
|
||||
|
||||
Run the interactive installer:
|
||||
|
||||
+7
-3
@@ -29,7 +29,7 @@ preview — you only change the URL.
|
||||
```text
|
||||
Set this folder up for HyperFrames and make me a video.
|
||||
|
||||
1. Run: npx hyperframes skills update
|
||||
1. If the HyperFrames plugin is installed, use its bundled skills and launcher. Otherwise run: npx hyperframes skills update
|
||||
2. Then, using /hyperframes, make a 10-second product intro for https://example.com
|
||||
3. Open the local preview when it is done and tell me the project folder.
|
||||
```
|
||||
@@ -37,9 +37,13 @@ Set this folder up for HyperFrames and make me a video.
|
||||
Swap `https://example.com` for the page you want the video to show. Everything
|
||||
below is the same thing done by hand, if you would rather see each step.
|
||||
|
||||
## 1. Install the skills
|
||||
## 1. Install the plugin or skills
|
||||
|
||||
Run this in your project folder:
|
||||
For Claude Code, Copilot, VS Code, Cursor, or Gemini CLI, start with the
|
||||
[plugin installation guide](/guides/plugins). It includes update settings for
|
||||
each agent. Claude's plugin router is `/hyperframes:hyperframes`.
|
||||
|
||||
For standalone skills, run this in your project folder:
|
||||
|
||||
```bash
|
||||
npx skills add heygen-com/hyperframes
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "hyperframes",
|
||||
"version": "0.8.81",
|
||||
"description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames."
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
"test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --import ./packages/cli/scripts/test-home.mjs --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
|
||||
"generate:previews": "tsx scripts/generate-template-previews.ts",
|
||||
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
|
||||
"package:agent-plugin": "node scripts/package-agent-plugin.mjs",
|
||||
"package:codex-plugin": "node scripts/package-codex-plugin.mjs",
|
||||
"upload:docs-images": "bash scripts/upload-docs-images.sh",
|
||||
"host:registry-assets": "bun run scripts/host-registry-assets.ts",
|
||||
|
||||
@@ -945,7 +945,9 @@ export default defineCommand({
|
||||
console.log("Get started:");
|
||||
console.log();
|
||||
if (skipSkills) {
|
||||
console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`);
|
||||
console.log(
|
||||
` ${c.accent("1.")} Use your HyperFrames plugin, or install standalone skills:`,
|
||||
);
|
||||
console.log(` ${c.accent("npx hyperframes skills update")}`);
|
||||
} else {
|
||||
console.log(
|
||||
|
||||
@@ -24,7 +24,11 @@ The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-
|
||||
|
||||
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
||||
|
||||
> **Skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
||||
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
|
||||
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
|
||||
> the plugin manager, not the standalone commands below.
|
||||
>
|
||||
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
||||
> the specific skill you need (the `/hyperframes` router does this automatically before
|
||||
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
|
||||
> everything already installed — neither pulls the full set. Restart the agent session so
|
||||
|
||||
@@ -24,7 +24,11 @@ The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-
|
||||
|
||||
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
||||
|
||||
> **Skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
||||
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
|
||||
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
|
||||
> the plugin manager, not the standalone commands below.
|
||||
>
|
||||
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
||||
> the specific skill you need (the `/hyperframes` router does this automatically before
|
||||
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
|
||||
> everything already installed — neither pulls the full set. Restart the agent session so
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "hyperframes",
|
||||
"description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
|
||||
"version": "0.8.81",
|
||||
"author": {
|
||||
"name": "HeyGen",
|
||||
"email": "hyperframes@heygen.com",
|
||||
"url": "https://hyperframes.heygen.com"
|
||||
},
|
||||
"homepage": "https://hyperframes.heygen.com",
|
||||
"repository": "https://github.com/heygen-com/hyperframes",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// guards: skills/embedded-captions/**, skills/hyperframes/scripts/plugin-cli.mjs, scripts/package-agent-plugin.mjs
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve, delimiter } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
function assertContributorOverrides(dir, scripts) {
|
||||
const override = join(dir, "contributor checkout");
|
||||
const built = join(override, "packages/cli/dist/cli.js");
|
||||
mkdirSync(join(built, ".."), { recursive: true });
|
||||
writeFileSync(built, "// built CLI fixture");
|
||||
for (const explicitArg of [true, false]) {
|
||||
const found = execFileSync(
|
||||
process.execPath,
|
||||
[join(scripts, "hf-cli.cjs"), ...(explicitArg ? [override] : [])],
|
||||
{ encoding: "utf8", env: { ...process.env, HYPERFRAMES_ROOT: explicitArg ? "" : override } },
|
||||
);
|
||||
assert.equal(found.trim(), built);
|
||||
}
|
||||
}
|
||||
|
||||
test("extracted captions helpers invoke the pinned CLI without a checkout", (t) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-caption-zip-"));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
execFileSync("unzip", ["-q", resolve("dist/hyperframes-agent-plugin.zip"), "-d", dir]);
|
||||
const root = join(dir, "hyperframes");
|
||||
const version = JSON.parse(readFileSync(join(root, "plugin.json"))).version;
|
||||
const scripts = join(root, "skills/embedded-captions/scripts");
|
||||
// Git-based installs include source directories but no built CLI, unlike ZIPs.
|
||||
mkdirSync(join(root, "packages/cli"), { recursive: true });
|
||||
assert.equal(
|
||||
execFileSync(process.execPath, [join(scripts, "hf-cli.cjs")], { encoding: "utf8" }).trim(),
|
||||
realpathSync(join(root, "skills/hyperframes/scripts/plugin-cli.mjs")),
|
||||
);
|
||||
assertContributorOverrides(dir, scripts);
|
||||
rmSync(join(root, "packages"), { recursive: true });
|
||||
const project = join(dir, "project with spaces");
|
||||
const bin = join(dir, "bin");
|
||||
mkdirSync(project);
|
||||
mkdirSync(bin);
|
||||
mkdirSync(join(dir, "home"));
|
||||
mkdirSync(join(project, "frames_fg"));
|
||||
writeFileSync(join(project, "source.mp4"), "fixture");
|
||||
writeFileSync(join(project, "audio.mp3"), "fixture");
|
||||
writeFileSync(join(project, "index.html"), "<html>caption fixture</html>");
|
||||
writeFileSync(join(project, "matte.fps"), "24");
|
||||
const log = join(dir, "invocation.json");
|
||||
writeFileSync(
|
||||
join(bin, "npx"),
|
||||
`#!${process.execPath}\nrequire('node:fs').writeFileSync(${JSON.stringify(log)}, JSON.stringify({args:process.argv.slice(2), skip:process.env.HYPERFRAMES_SKIP_SKILLS})); process.exit(23);`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
writeFileSync(join(bin, "ffmpeg"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
writeFileSync(join(bin, "ffprobe"), "#!/bin/sh\nprintf '24/1\\n24/1\\n'\n", { mode: 0o755 });
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: join(dir, "home"),
|
||||
HYPERFRAMES_ROOT: "",
|
||||
TRANSCRIBE_ENGINE: "whisper",
|
||||
PATH: `${bin}${delimiter}${process.env.PATH}`,
|
||||
};
|
||||
for (const [file, command, cliCommand] of [
|
||||
["matte.cjs", process.execPath, "remove-background"],
|
||||
["transcribe.cjs", process.execPath, "transcribe"],
|
||||
["render-and-composite.sh", "bash", "render"],
|
||||
]) {
|
||||
rmSync(log, { force: true });
|
||||
const result = spawnSync(command, [join(scripts, file), project], {
|
||||
cwd: dir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
timeout: 20000,
|
||||
});
|
||||
assert.notEqual(result.status, 0, "fake CLI failure must propagate");
|
||||
assert.ok(existsSync(log), `${file}: ${result.stdout} ${result.stderr}`);
|
||||
const call = JSON.parse(readFileSync(log, "utf8"));
|
||||
assert.deepEqual(
|
||||
call.args.slice(0, 3),
|
||||
["--yes", `hyperframes@${version}`, cliCommand],
|
||||
result.stderr,
|
||||
);
|
||||
assert.equal(call.skip, "1");
|
||||
}
|
||||
});
|
||||
|
||||
function cleanupRenderFixture(dir, pidFile) {
|
||||
if (existsSync(pidFile)) {
|
||||
const { npm, render } = JSON.parse(readFileSync(pidFile));
|
||||
for (const pid of [npm, render]) {
|
||||
try {
|
||||
process.kill(pid, "SIGKILL");
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function assertProcessesExited(pidFile) {
|
||||
const pids = JSON.parse(readFileSync(pidFile));
|
||||
for (const pid of Object.values(pids)) {
|
||||
const status = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8" });
|
||||
assert.ok(
|
||||
status.status === 1 || /^\s*Z/.test(status.stdout),
|
||||
`PID ${pid} survives: ${status.stdout} ${status.stderr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{ code: 0, expectedStatus: 0 },
|
||||
{ code: 23, expectedStatus: 1 },
|
||||
...["INT", "TERM", "HUP"].map((cancel) => ({ cancel, expectedStatus: 0 })),
|
||||
];
|
||||
for (const { code, cancel, expectedStatus } of cases) {
|
||||
test(`ZIP render waits for CLI completion: code=${code}, cancel=${cancel}`, async (t) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-caption-render-"));
|
||||
const pidFile = join(dir, "pids.json");
|
||||
const unrelated = spawn(process.execPath, ["-e", "setInterval(()=>{},1000)"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
t.after(() => unrelated.kill());
|
||||
t.after(() => cleanupRenderFixture(dir, pidFile));
|
||||
execFileSync("unzip", ["-q", resolve("dist/hyperframes-agent-plugin.zip"), "-d", dir]);
|
||||
const script = join(
|
||||
dir,
|
||||
"hyperframes/skills/embedded-captions/scripts/render-and-composite.sh",
|
||||
);
|
||||
const project = join(dir, "project");
|
||||
const bin = join(dir, "bin");
|
||||
mkdirSync(join(project, "frames_fg"), { recursive: true });
|
||||
mkdirSync(bin);
|
||||
mkdirSync(join(dir, "home"));
|
||||
writeFileSync(join(project, "index.html"), "<html>fixture</html>");
|
||||
writeFileSync(join(project, "matte.fps"), "24");
|
||||
const output = join(project, "bg_plus_caps.mp4");
|
||||
const child = join(dir, "child.cjs");
|
||||
writeFileSync(
|
||||
child,
|
||||
"const fs=require('node:fs'); setInterval(()=>fs.appendFileSync(process.argv[2],'x'),50);",
|
||||
);
|
||||
writeFileSync(
|
||||
join(bin, "npx"),
|
||||
`#!${process.execPath}
|
||||
const fs=require('node:fs');
|
||||
const child=require('node:child_process').spawn(process.execPath,[${JSON.stringify(child)},${JSON.stringify(output)}],{stdio:'ignore'});
|
||||
fs.writeFileSync(${JSON.stringify(output)},Buffer.alloc(1200000));
|
||||
fs.writeFileSync(${JSON.stringify(pidFile)},JSON.stringify({wrapper:process.ppid,npm:process.pid,render:child.pid}));
|
||||
${cancel ? "setInterval(()=>{},1000);" : `setTimeout(()=>{child.kill(); child.once('exit',()=>process.exit(${code}));},1200);`}
|
||||
`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
// Only downstream media operations are stubbed; process cleanup and PTY are real.
|
||||
writeFileSync(join(bin, "ffprobe"), "#!/bin/sh\necho 2\n", { mode: 0o755 });
|
||||
writeFileSync(join(bin, "ffmpeg"), '#!/bin/bash\ntouch "${@: -1}"\n', { mode: 0o755 });
|
||||
const terminal = `
|
||||
import os, pty, select, signal, sys, time
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.execvp("bash", ["bash", sys.argv[1], sys.argv[2]])
|
||||
deadline = time.monotonic() + 15
|
||||
sent = False
|
||||
while time.monotonic() < deadline:
|
||||
if not sent and os.path.exists(sys.argv[3]):
|
||||
if sys.argv[4] == "INT": os.write(fd, b"\\x03")
|
||||
else: os.killpg(pid, getattr(signal, "SIG" + sys.argv[4]))
|
||||
sent = True
|
||||
if select.select([fd], [], [], 0.05)[0]:
|
||||
try: os.read(fd, 65536)
|
||||
except OSError: pass
|
||||
ended, status = os.waitpid(pid, os.WNOHANG)
|
||||
if ended:
|
||||
time.sleep(0.5)
|
||||
sys.exit(0 if sent and os.waitstatus_to_exitcode(status) != 0 else 1)
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
sys.exit(2)
|
||||
`;
|
||||
const result = spawnSync(
|
||||
cancel ? "python3" : "bash",
|
||||
cancel ? ["-c", terminal, script, project, pidFile, cancel] : [script, project],
|
||||
{
|
||||
encoding: "utf8",
|
||||
timeout: 20000,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: join(dir, "home"),
|
||||
HYPERFRAMES_ROOT: "",
|
||||
PATH: `${bin}${delimiter}${process.env.PATH}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(result.error, undefined, result.stderr);
|
||||
assert.equal(result.status, expectedStatus, result.stdout + result.stderr);
|
||||
assert.equal(unrelated.exitCode, null);
|
||||
assert.doesNotThrow(() => process.kill(unrelated.pid, 0));
|
||||
assert.equal(existsSync(join(project, "final.mp4")), code === 0);
|
||||
assertProcessesExited(pidFile);
|
||||
const before = readFileSync(output).length;
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
assert.equal(readFileSync(output).length, before, "render still writes output after returning");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const MANIFESTS = [
|
||||
"plugin.json",
|
||||
"gemini-extension.json",
|
||||
".claude-plugin/plugin.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
];
|
||||
|
||||
function validateManifests(read) {
|
||||
const portable = read("plugin.json");
|
||||
assert.equal(
|
||||
portable.$schema,
|
||||
"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"Unexpected Agent Plugins schema",
|
||||
);
|
||||
assert.equal(portable.name, "hyperframes", "Invalid plugin identity");
|
||||
assert.match(
|
||||
String(portable.version),
|
||||
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/,
|
||||
"Invalid plugin release version",
|
||||
);
|
||||
for (const path of MANIFESTS) {
|
||||
const manifest = read(path);
|
||||
assert.equal(manifest.name, portable.name, `Plugin identity mismatch: ${path}`);
|
||||
assert.equal(manifest.version, portable.version, `Plugin version mismatch: ${path}`);
|
||||
}
|
||||
assert.equal(
|
||||
read("packages/cli/package.json").version,
|
||||
portable.version,
|
||||
"Plugin and CLI versions must match",
|
||||
);
|
||||
return portable;
|
||||
}
|
||||
|
||||
function payloadPaths(read) {
|
||||
const codex = read(".codex-plugin/plugin.json");
|
||||
const assets = [
|
||||
...new Set(
|
||||
[
|
||||
codex.interface.logo,
|
||||
codex.interface.composerIcon,
|
||||
read(".cursor-plugin/plugin.json").logo,
|
||||
].map((p) => p.replace(/^\.\//, "")),
|
||||
),
|
||||
];
|
||||
if (assets.some((p) => !/^assets\/[\w.-]+$/.test(p))) throw new Error("Unsafe plugin asset path");
|
||||
return [...MANIFESTS, ...assets, "LICENSE", "skills", "skills-manifest.json"];
|
||||
}
|
||||
|
||||
// Package committed files only, including metadata. Dirty manifests must not
|
||||
// describe a different release from the files git archive actually exports.
|
||||
export function packagePlugin(root = ROOT) {
|
||||
const git = (...args) => execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
||||
const read = (path) => JSON.parse(git("show", `HEAD:${path}`));
|
||||
const portable = validateManifests(read);
|
||||
const source = git("rev-parse", "HEAD").trim();
|
||||
const published = Object.keys(read("skills-manifest.json").skills).sort();
|
||||
const skills = git("ls-tree", "-r", "--name-only", "HEAD", "skills").trim().split("\n");
|
||||
const found = skills
|
||||
.filter((p) => /^skills\/[^/]+\/SKILL.md$/.test(p))
|
||||
.map((p) => p.split("/")[1])
|
||||
.sort();
|
||||
if (JSON.stringify(found) !== JSON.stringify(published))
|
||||
throw new Error("Plugin skills differ from the published catalog");
|
||||
const paths = payloadPaths(read);
|
||||
for (const path of paths) git("cat-file", "-e", `HEAD:${path}`);
|
||||
const out = join(root, "dist");
|
||||
mkdirSync(out, { recursive: true });
|
||||
const archive = join(out, "hyperframes-agent-plugin.zip");
|
||||
git(
|
||||
"archive",
|
||||
"--format=zip",
|
||||
"--prefix=hyperframes/",
|
||||
"--output",
|
||||
archive,
|
||||
"HEAD",
|
||||
"--",
|
||||
...paths,
|
||||
);
|
||||
const bytes = readFileSync(archive);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const metadata = {
|
||||
name: portable.name,
|
||||
version: portable.version,
|
||||
source,
|
||||
sha256,
|
||||
bytes: bytes.length,
|
||||
skills: published,
|
||||
};
|
||||
writeFileSync(
|
||||
join(out, "hyperframes-agent-plugin.json"),
|
||||
JSON.stringify(metadata, null, 2) + "\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(out, "hyperframes-agent-plugin.sha256"),
|
||||
`${sha256} hyperframes-agent-plugin.zip\n`,
|
||||
);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url) {
|
||||
console.log(JSON.stringify(packagePlugin(), null, 2));
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// guards: scripts/package-agent-plugin.mjs, plugin.json, gemini-extension.json, .claude-plugin/**, .codex-plugin/**, .cursor-plugin/**, skills/**, skills-manifest.json, assets/**, LICENSE, packages/cli/package.json
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { test } from "node:test";
|
||||
import { packagePlugin } from "./package-agent-plugin.mjs";
|
||||
|
||||
function fixture(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), "hf-package-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const git = (...args) =>
|
||||
execFileSync("git", ["-c", "core.hooksPath=/dev/null", ...args], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const write = (path, value) => {
|
||||
mkdirSync(join(root, path, ".."), { recursive: true });
|
||||
writeFileSync(join(root, path), typeof value === "string" ? value : JSON.stringify(value));
|
||||
};
|
||||
git("init", "-q");
|
||||
git("config", "user.name", "Test");
|
||||
git("config", "user.email", "test@example.com");
|
||||
const manifest = { name: "hyperframes", version: "1.2.3" };
|
||||
write("plugin.json", {
|
||||
...manifest,
|
||||
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
});
|
||||
write("gemini-extension.json", manifest);
|
||||
write(".claude-plugin/plugin.json", manifest);
|
||||
write(".cursor-plugin/plugin.json", { ...manifest, logo: "assets/logo.png" });
|
||||
write(".codex-plugin/plugin.json", {
|
||||
...manifest,
|
||||
interface: { logo: "./assets/logo.png", composerIcon: "./assets/icon.png" },
|
||||
});
|
||||
write("packages/cli/package.json", manifest);
|
||||
write("skills-manifest.json", { skills: { hyperframes: {} } });
|
||||
write("skills/hyperframes/SKILL.md", "bundled skill");
|
||||
write(".claude/skills/internal/SKILL.md", "private contributor workflow");
|
||||
write("assets/logo.png", "logo");
|
||||
write("assets/icon.png", "icon");
|
||||
write("LICENSE", "license");
|
||||
git("add", ".");
|
||||
git("commit", "-qm", "fixture");
|
||||
return { root, git, write };
|
||||
}
|
||||
|
||||
test("archives committed release, excludes internals, and records reproducible provenance", (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write("plugin.json", { version: "dirty" });
|
||||
write("skills/hyperframes/SKILL.md", "uncommitted change");
|
||||
const first = packagePlugin(root);
|
||||
const second = packagePlugin(root);
|
||||
assert.deepEqual(first, second);
|
||||
assert.equal(first.version, "1.2.3");
|
||||
assert.deepEqual(first.skills, ["hyperframes"]);
|
||||
const zip = join(root, "dist/hyperframes-agent-plugin.zip");
|
||||
assert.equal(first.sha256, createHash("sha256").update(readFileSync(zip)).digest("hex"));
|
||||
const files = execFileSync("unzip", ["-Z1", zip], { encoding: "utf8" });
|
||||
assert.match(files, /hyperframes\/LICENSE/);
|
||||
assert.doesNotMatch(files, /internal|packages\/|marketplace.json/);
|
||||
assert.equal(
|
||||
execFileSync("unzip", ["-p", zip, "hyperframes/skills/hyperframes/SKILL.md"], {
|
||||
encoding: "utf8",
|
||||
}),
|
||||
"bundled skill",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects mismatched client versions before creating an artifact", (t) => {
|
||||
const { root, git, write } = fixture(t);
|
||||
write("gemini-extension.json", { name: "hyperframes", version: "1.2.4" });
|
||||
git("add", ".");
|
||||
git("commit", "-qm", "mismatch");
|
||||
assert.throws(() => packagePlugin(root), /mismatch: gemini-extension.json/);
|
||||
});
|
||||
|
||||
test("rejects a skill missing from the published catalog", (t) => {
|
||||
const { root, git, write } = fixture(t);
|
||||
write("skills/unpublished/SKILL.md", "not published");
|
||||
git("add", ".");
|
||||
git("commit", "-qm", "unpublished");
|
||||
assert.throws(() => packagePlugin(root), /published catalog/);
|
||||
});
|
||||
@@ -145,6 +145,8 @@ describe("changed-path guard", () => {
|
||||
"packages/core/package.json",
|
||||
"packages/sdk/package.json",
|
||||
".claude-plugin/plugin.json",
|
||||
"plugin.json",
|
||||
"gemini-extension.json",
|
||||
"releases/v1.2.3.md",
|
||||
],
|
||||
allowed,
|
||||
|
||||
@@ -36,7 +36,13 @@ const PACKAGES = [
|
||||
"packages/sdk",
|
||||
];
|
||||
|
||||
const PLUGINS = [".claude-plugin", ".codex-plugin", ".cursor-plugin"];
|
||||
const PLUGINS = [
|
||||
".claude-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
"plugin.json",
|
||||
"gemini-extension.json",
|
||||
];
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..");
|
||||
export const CHANGELOG_REVIEW_TODO = "<!-- TODO: write a 1-2 sentence release summary here. -->";
|
||||
@@ -118,7 +124,7 @@ function updatePluginVersions(version: string) {
|
||||
// short arrays inline, but JSON.stringify expands them, which would fail the
|
||||
// pre-commit format check on the release commit this script creates.
|
||||
for (const plugin of PLUGINS) {
|
||||
const pluginPath = join(ROOT, plugin, "plugin.json");
|
||||
const pluginPath = join(ROOT, plugin);
|
||||
const text = readFileSync(pluginPath, "utf-8");
|
||||
const oldVersion = text.match(/"version"\s*:\s*"([^"]*)"/)?.[1] ?? "unknown";
|
||||
writeFileSync(pluginPath, text.replace(/("version"\s*:\s*)"[^"]*"/, `$1"${version}"`));
|
||||
@@ -311,7 +317,7 @@ export function docsChangelogEntryHasGeneratedTodo(content: string, marker: stri
|
||||
export function releaseAllowedPaths(version: string) {
|
||||
return [
|
||||
...PACKAGES.map((pkg) => join(pkg, "package.json")),
|
||||
...PLUGINS.map((plugin) => join(plugin, "plugin.json")),
|
||||
...PLUGINS,
|
||||
"docs/changelog.mdx",
|
||||
join("releases", `v${version}.md`),
|
||||
];
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"source": "heygen-com/hyperframes",
|
||||
"skills": {
|
||||
"embedded-captions": {
|
||||
"hash": "c6cac07dbd207deb",
|
||||
"files": 142
|
||||
"hash": "6a9850a8d250780e",
|
||||
"files": 143
|
||||
},
|
||||
"faceless-explainer": {
|
||||
"hash": "7fda7a8bb75acabf",
|
||||
@@ -18,7 +18,7 @@
|
||||
"files": 5
|
||||
},
|
||||
"hyperframes": {
|
||||
"hash": "93ee5e4fad423610",
|
||||
"hash": "8dffb9cd5b333c9c",
|
||||
"files": 29
|
||||
},
|
||||
"hyperframes-animation": {
|
||||
@@ -54,8 +54,8 @@
|
||||
"files": 1
|
||||
},
|
||||
"media-use": {
|
||||
"hash": "912204bb8f5fd200",
|
||||
"files": 100
|
||||
"hash": "ebef14be0621a731",
|
||||
"files": 101
|
||||
},
|
||||
"motion-graphics": {
|
||||
"hash": "3d3a35ffbe8b7e3e",
|
||||
|
||||
@@ -19,6 +19,27 @@ description: >
|
||||
|
||||
---
|
||||
|
||||
## Runtime prerequisites
|
||||
|
||||
Plugin installs use the bundled, manifest-pinned CLI for matting, transcription,
|
||||
and rendering; no source checkout is required. The local preview and caption
|
||||
measurement helpers also need Sharp, Puppeteer (with its Chromium browser), and
|
||||
GSAP. Install these in the **caption project**, not inside the read-only plugin:
|
||||
|
||||
```bash
|
||||
npm install --prefix <project> --save-dev --save-exact sharp@0.35.3 puppeteer@25.8.0 gsap@3.15.0
|
||||
```
|
||||
|
||||
Keep the project's lockfile. If these dependencies already exist, use its locked
|
||||
versions instead of overwriting them. Bash and FFmpeg/ffprobe must be on PATH.
|
||||
Matting and transcription may download their own models on first use.
|
||||
|
||||
Rendering waits for the CLI to exit successfully before compositing. The old
|
||||
`HF_TIMEOUT_S` shell watchdog is no longer used: a large partial file is not proof
|
||||
that rendering finished. An explicit built-checkout argument or `HYPERFRAMES_ROOT`
|
||||
selects the contributor CLI instead of the plugin pin. Cancel a stalled render normally through the CLI/terminal;
|
||||
the caption helper does not force-kill or recover a render from a process snapshot.
|
||||
|
||||
## Operational flow (TL;DR)
|
||||
|
||||
Routed through `/hyperframes`, the intent layer confirms only the input (which clip) and **announces** the identity pick as a deferred ask — the shortlist needs the probed clip, so it stays at step 1 below; the layer's run-shape questions don't apply (the footage is untouched, there is no storyboard to review). A `BRIEF.md`, when present, carries the confirmed input and any user notes — read it first.
|
||||
@@ -252,8 +273,8 @@ The full **embed-track** playbook lives in **[references/composition-craft.md](r
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **hyperframes**, built (`packages/cli/dist/cli.js`). Scripts auto-resolve the checkout: `HYPERFRAMES_ROOT` env → repo root if this skill ships _inside_ hyperframes → `~/Downloads/hyperframes`. Build with `bun install && bun run build`.
|
||||
- **Node-first; two Python touchpoints via `uvx` (no manual installs):** transcription runs WhisperX through `uvx` (word-level timings; falls back per SKILL §transcription), and Theme's `drawon` setpiece shells `python3 scripts/gen-stroke-path.py` at compile time. Everything else runs on the toolchain hyperframes already ships: matting via the hyperframes CLI's **`remove-background`** (u2net_human_seg; weights auto-download once, ~168 MB, to `~/.cache/hyperframes/`), image/alpha math via **`sharp`**, layout/occlusion/overflow via **`puppeteer`**, plus **`ffmpeg`**. The scripts auto-resolve these from the hyperframes checkout — nothing extra to install.
|
||||
- **HyperFrames CLI:** plugin installs use the bundled manifest-pinned launcher. Source contributors can use a built checkout (`packages/cli/dist/cli.js`) via `HYPERFRAMES_ROOT`, the skill’s source tree, or `~/Downloads/hyperframes`.
|
||||
- **Node-first; two Python touchpoints via `uvx` (no manual installs):** transcription runs WhisperX through `uvx` (word-level timings; falls back per SKILL §transcription), and Theme's `drawon` setpiece shells `python3 scripts/gen-stroke-path.py` at compile time. Everything else runs on the toolchain hyperframes already ships: matting via the hyperframes CLI's **`remove-background`** (u2net_human_seg; weights auto-download once, ~168 MB, to `~/.cache/hyperframes/`), image/alpha math via **`sharp`**, layout/occlusion/overflow via **`puppeteer`**, plus **`ffmpeg`**. Install Sharp, Puppeteer, and GSAP in the caption project as described in **Runtime prerequisites** above. The helpers check that project first and retain checkout dependency lookup for source contributors.
|
||||
- **Transcription = WhisperX via `uvx`** (word-level timings + alignment; no manual install — `transcribe.cjs` drives `uvx whisperx`). Falls back to an existing word-level `transcript.json` if present.
|
||||
- **Source video** — `matte.cjs` / `transcribe.cjs` auto-resolve `source.mp4` (or glob the clip / read `hyperframes.json`), so `hyperframes init --video X.mp4` needs no manual rename.
|
||||
- **fps** — `matte.cjs` extracts at the source's native rate and records `matte.fps`; `render-and-composite.sh` uses that so the matte stays frame-aligned.
|
||||
|
||||
@@ -11,6 +11,7 @@ const cp = require("child_process");
|
||||
|
||||
function hfResolve(pkg) {
|
||||
const roots = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "..", "..", ".."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
@@ -29,7 +30,9 @@ function hfResolve(pkg) {
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
console.error(`[v2] cannot find ${pkg} — set HYPERFRAMES_ROOT`);
|
||||
console.error(
|
||||
`[v2] cannot find ${pkg} — install project dependencies per embedded-captions SKILL.md Runtime prerequisites`,
|
||||
);
|
||||
process.exit(3);
|
||||
}
|
||||
const sharp = hfResolve("sharp");
|
||||
|
||||
@@ -18,6 +18,7 @@ const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const HF_ROOTS = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "../../.."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
|
||||
@@ -20,6 +20,7 @@ const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const HF_ROOTS = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "../../.."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
@@ -154,7 +155,10 @@ async function main() {
|
||||
const railPath = path.join(project, "rail.html");
|
||||
if (!fs.existsSync(railPath) || !fs.existsSync(indexPath))
|
||||
ok("[rail-climax] no rail.html+index.html — not Standard, skipping");
|
||||
if (!puppeteer) ok("[rail-climax] puppeteer unavailable — skipping (set HYPERFRAMES_ROOT)");
|
||||
if (!puppeteer)
|
||||
ok(
|
||||
"[rail-climax] puppeteer unavailable — skipping (install project dependencies per embedded-captions SKILL.md Runtime prerequisites)",
|
||||
);
|
||||
|
||||
const exe =
|
||||
process.platform === "darwin"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Resolve CLI ownership once for matting, transcription, and shell rendering.
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
function hfCli(checkout) {
|
||||
const root = path.resolve(__dirname, "../../..");
|
||||
for (const dir of [checkout, process.env.HYPERFRAMES_ROOT].filter(Boolean)) {
|
||||
const cli = path.join(dir, "packages/cli/dist/cli.js");
|
||||
if (fs.existsSync(cli)) return cli;
|
||||
}
|
||||
const manifests = [
|
||||
"plugin.json",
|
||||
".claude-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
"gemini-extension.json",
|
||||
];
|
||||
if (
|
||||
!fs.existsSync(path.join(root, "packages/cli/dist/cli.js")) &&
|
||||
manifests.some((p) => fs.existsSync(path.join(root, p)))
|
||||
) {
|
||||
return path.join(root, "skills/hyperframes/scripts/plugin-cli.mjs");
|
||||
}
|
||||
for (const dir of [root, path.join(os.homedir(), "Downloads/hyperframes")].filter(Boolean)) {
|
||||
const cli = path.join(dir, "packages/cli/dist/cli.js");
|
||||
if (fs.existsSync(cli)) return cli;
|
||||
}
|
||||
throw new Error(
|
||||
"HyperFrames CLI unavailable: install the full plugin or set HYPERFRAMES_ROOT to a built checkout.",
|
||||
);
|
||||
}
|
||||
module.exports = { hfCli };
|
||||
if (require.main === module) console.log(hfCli(process.argv[2]));
|
||||
@@ -30,19 +30,7 @@ const fs = require("fs");
|
||||
const os = require("os");
|
||||
const cp = require("child_process");
|
||||
|
||||
function hfCli() {
|
||||
const roots = [
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "..", "..", ".."), // skills/embedded-captions/scripts → repo root if in-repo
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
].filter(Boolean);
|
||||
for (const root of roots) {
|
||||
const cli = path.join(root, "packages", "cli", "dist", "cli.js");
|
||||
if (fs.existsSync(cli)) return cli;
|
||||
}
|
||||
console.error("[matte] cannot find hyperframes cli — set HYPERFRAMES_ROOT to a built checkout");
|
||||
process.exit(3);
|
||||
}
|
||||
const { hfCli } = require("./hf-cli.cjs");
|
||||
|
||||
function ensureSource(project) {
|
||||
const src = path.join(project, "source.mp4");
|
||||
|
||||
@@ -22,6 +22,7 @@ const os = require("os");
|
||||
// HYPERFRAMES_ROOT; standalone we also try the in-repo path + ~/Downloads, and
|
||||
// accept ANY puppeteer@* the bun store holds (not a pinned version).
|
||||
const HF_ROOTS = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "../../.."), // skills/embedded-captions/scripts → repo root if in-repo
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
@@ -54,7 +55,7 @@ for (const root of HF_ROOTS) {
|
||||
}
|
||||
if (!puppeteer) {
|
||||
console.error(
|
||||
"[measure] could not locate puppeteer — set HYPERFRAMES_ROOT to a built hyperframes checkout",
|
||||
"[measure] could not locate puppeteer — install project dependencies per embedded-captions SKILL.md Runtime prerequisites",
|
||||
);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ function withPreviewGsapSri(html, gsapSource) {
|
||||
}
|
||||
|
||||
const HF_ROOTS = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "../../.."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
@@ -82,7 +83,9 @@ for (const r of HF_ROOTS) {
|
||||
}
|
||||
}
|
||||
if (require.main === module && (!puppeteer || !sharp)) {
|
||||
console.error("[preview] need puppeteer+sharp — set HYPERFRAMES_ROOT");
|
||||
console.error(
|
||||
"[preview] need puppeteer+sharp — install project dependencies per embedded-captions SKILL.md Runtime prerequisites",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,21 +12,10 @@ set -euo pipefail
|
||||
PROJECT="${1:?usage: render-and-composite.sh <project-dir> [hyperframes-repo]}"
|
||||
PROJECT="$(cd "$PROJECT" && pwd)"
|
||||
|
||||
# Resolve the hyperframes checkout. Candidate order:
|
||||
# 1. arg 2 2. $HYPERFRAMES_ROOT 3. repo root if this skill ships INSIDE the
|
||||
# hyperframes repo (skills/embedded-captions/scripts → ../../..) 4. ~/Downloads/hyperframes
|
||||
SKILL_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HF=""
|
||||
for cand in "${2:-}" "${HYPERFRAMES_ROOT:-}" "$(cd "$SKILL_SCRIPT_DIR/../../.." 2>/dev/null && pwd)" "$HOME/Downloads/hyperframes"; do
|
||||
if [[ -n "$cand" && -f "$cand/packages/cli/dist/cli.js" ]]; then HF="$cand"; break; fi
|
||||
done
|
||||
if [[ -z "$HF" ]]; then
|
||||
echo "[render] hyperframes CLI not found. Set HYPERFRAMES_ROOT to your hyperframes" >&2
|
||||
echo " checkout (needs packages/cli/dist/cli.js — 'bun install && bun run build')." >&2
|
||||
exit 1
|
||||
fi
|
||||
export HYPERFRAMES_ROOT="$HF" # so the occlusion gate's measure-layout.cjs finds puppeteer too
|
||||
HF_CLI="$HF/packages/cli/dist/cli.js"
|
||||
HF_CLI="$(node "$SKILL_SCRIPT_DIR/hf-cli.cjs" "${2:-}")"
|
||||
# Retain the explicit dependency-root override for source-checkout users.
|
||||
if [[ -n "${2:-}" ]]; then export HYPERFRAMES_ROOT="$2"; fi
|
||||
if [[ ! -d "$PROJECT/frames_fg" ]]; then
|
||||
echo "[render] missing matte frames at $PROJECT/frames_fg — run matte.cjs first" >&2
|
||||
exit 1
|
||||
@@ -237,49 +226,16 @@ echo "[render] snapshot → history/index-${STAMP}.html"
|
||||
|
||||
echo "[render] hyperframes render @ ${FPS}fps"
|
||||
|
||||
# Hyperframes occasionally hangs on Chromium shutdown *after* the output file
|
||||
# is successfully written (seen multiple times on 15–30s clips). Without a
|
||||
# guard the shell waits forever. This helper enforces a max wall-clock budget,
|
||||
# and if the output is already on disk when we hit it, treats the run as
|
||||
# successful and kills the zombie. Tune HF_TIMEOUT_S via env if needed.
|
||||
# Default SCALES with clip size: two parallel Chromium passes on a long clip
|
||||
# legitimately exceed a fixed 240s (a 38s/1151-frame render was killed at 244s
|
||||
# while healthy). ~1.5s per source frame, floor 240s.
|
||||
N_FRAMES="$(ls "$PROJECT/frames_fg" 2>/dev/null | wc -l | tr -d ' ')"
|
||||
HF_TIMEOUT_S="${HF_TIMEOUT_S:-$(( N_FRAMES * 3 / 2 > 240 ? N_FRAMES * 3 / 2 : 240 ))}"
|
||||
# hf_render_dir: render one hyperframes composition.
|
||||
# args: <output.mp4> <label> <project_dir>
|
||||
# watches for the Chromium-shutdown-hang; if output file exists and is >1MB
|
||||
# past timeout, treats as success and kills the zombie.
|
||||
# Wait for the renderer's real exit status. File size cannot prove completion,
|
||||
# and a portable process snapshot cannot safely recover a timed-out render tree.
|
||||
# Cancellation/browser cleanup belongs to the CLI that owns those processes.
|
||||
hf_render_dir() {
|
||||
local out="$1" label="$2" proj="$3" fmt="${4:-}"
|
||||
# bash 3.2 (macOS) throws on empty-array expansion under `set -u`, so branch
|
||||
# explicitly instead of splatting an optional --format array.
|
||||
if [[ -n "$fmt" ]]; then
|
||||
node "$HF_CLI" render --skill=embedded-captions --dir "$proj" --fps "$FPS" --format "$fmt" --crf 11 -o "$out" &
|
||||
node "$HF_CLI" render --skill=embedded-captions --dir "$proj" --fps "$FPS" --format "$fmt" --crf 11 -o "$out" || return $?
|
||||
else
|
||||
node "$HF_CLI" render --skill=embedded-captions --dir "$proj" --fps "$FPS" --crf 11 -o "$out" &
|
||||
node "$HF_CLI" render --skill=embedded-captions --dir "$proj" --fps "$FPS" --crf 11 -o "$out" || return $?
|
||||
fi
|
||||
local pid=$! start=$SECONDS elapsed
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
elapsed=$((SECONDS - start))
|
||||
if (( elapsed > HF_TIMEOUT_S )); then
|
||||
local sz=0
|
||||
[[ -f "$out" ]] && sz=$(stat -f%z "$out" 2>/dev/null || echo 0)
|
||||
if (( sz > 1000000 )); then
|
||||
echo "[render] ${label}: node hung ${elapsed}s after shutdown (output ${sz}B exists, treating as success)"
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
pkill -9 -f "puppeteer_dev_chrome_profile" 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
echo "[render] ${label}: hung ${elapsed}s with no output — killing and failing" >&2
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
pkill -9 -f "puppeteer_dev_chrome_profile" 2>/dev/null
|
||||
return 2
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
wait "$pid" 2>/dev/null
|
||||
[[ -f "$out" ]]
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const THRESH = 30 / 255; // a cell is "subject" if ≥12% covered at any sampled
|
||||
const SAMPLES = 48; // frames cached across the clip (windows aggregate the cached grids)
|
||||
|
||||
const HF_ROOTS = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "../../.."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
@@ -558,7 +559,9 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
if (!sharp) {
|
||||
console.error("[safe-zones] sharp unavailable — set HYPERFRAMES_ROOT");
|
||||
console.error(
|
||||
"[safe-zones] sharp unavailable — install project dependencies per embedded-captions SKILL.md Runtime prerequisites",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,20 +9,10 @@
|
||||
*/
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const cp = require("child_process");
|
||||
|
||||
function hfRoot() {
|
||||
const roots = [
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "..", "..", ".."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
].filter(Boolean);
|
||||
for (const r of roots)
|
||||
if (fs.existsSync(path.join(r, "packages", "cli", "dist", "cli.js"))) return r;
|
||||
console.error("[transcribe] hyperframes CLI not found — set HYPERFRAMES_ROOT");
|
||||
process.exit(3);
|
||||
}
|
||||
const { hfCli } = require("./hf-cli.cjs");
|
||||
|
||||
function ensureSource(project) {
|
||||
const src = path.join(project, "source.mp4");
|
||||
if (fs.existsSync(src)) return src;
|
||||
@@ -247,7 +237,7 @@ function main() {
|
||||
|
||||
if (!words) {
|
||||
// run hyperframes Whisper → writes a flat word array to <dir>/transcript.json
|
||||
const cli = path.join(hfRoot(), "packages", "cli", "dist", "cli.js");
|
||||
const cli = hfCli();
|
||||
const args = ["transcribe", audio, "-d", project, "--json", "--model", model];
|
||||
if (language) args.push("--language", language);
|
||||
let info = {};
|
||||
|
||||
@@ -28,7 +28,7 @@ in every workflow and reference:
|
||||
latest. Existing project dependency files and lockfiles must not be overwritten
|
||||
to match the plugin; surface a runtime incompatibility before changing them.
|
||||
|
||||
- For bundled Node helpers that bootstrap HyperFrames packages, use the same
|
||||
- For bundled Node helpers, use the same
|
||||
launcher with `--script <absolute-script-path> <args...>`. This passes the
|
||||
release version to their dependency loader. External providers, registry
|
||||
downloads, and existing project dependencies have their own versions; a plugin
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
test(
|
||||
"plugin resolver uses the release CLI without a repository checkout",
|
||||
{ skip: process.platform === "win32" },
|
||||
(t) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-resolve-plugin-"));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
mkdirSync(join(dir, "lib"));
|
||||
for (const file of ["resolve.mjs", "lib/npx-sync.mjs"]) {
|
||||
copyFileSync(fileURLToPath(new URL(file, import.meta.url)), join(dir, file));
|
||||
}
|
||||
writeFileSync(
|
||||
join(dir, "npx"),
|
||||
`#!${process.execPath}\nconsole.log(JSON.stringify({args:process.argv.slice(2),skip:process.env.HYPERFRAMES_SKIP_SKILLS}));process.exit(7);`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const result = spawnSync(process.execPath, [join(dir, "resolve.mjs"), "--intent", "a & b"], {
|
||||
cwd: dir,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${dir}${delimiter}${process.env.PATH}`,
|
||||
HYPERFRAMES_PLUGIN_VERSION: "1.2.3",
|
||||
HYPERFRAMES_SKIP_SKILLS: "1",
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, 7, result.stderr);
|
||||
assert.deepEqual(JSON.parse(result.stdout), {
|
||||
args: ["--yes", "hyperframes@1.2.3", "media-use", "resolve", "--intent", "a & b"],
|
||||
skip: "1",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("helper package override keeps a contributor's local CLI", (t) => {
|
||||
const root = mkdtempSync(join(tmpdir(), "hf-resolve-local-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const scripts = join(root, "skills/media-use/scripts");
|
||||
mkdirSync(join(scripts, "lib"), { recursive: true });
|
||||
mkdirSync(join(root, "packages/cli/dist"), { recursive: true });
|
||||
for (const file of ["resolve.mjs", "lib/npx-sync.mjs"]) {
|
||||
copyFileSync(fileURLToPath(new URL(file, import.meta.url)), join(scripts, file));
|
||||
}
|
||||
writeFileSync(join(root, "packages/cli/dist/cli.js"), 'console.log("local-cli");');
|
||||
const result = spawnSync(process.execPath, [join(scripts, "resolve.mjs")], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
HYPERFRAMES_PLUGIN_VERSION: "",
|
||||
HYPERFRAMES_SKILL_PKG_VERSION: "0.8.79",
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout.trim(), "local-cli");
|
||||
});
|
||||
@@ -3,6 +3,20 @@ import { spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveNpxInvocation } from "./lib/npx-sync.mjs";
|
||||
|
||||
// Installed plugins have no monorepo packages/ tree. The plugin launcher passes
|
||||
// the release version and suppression environment through to this legacy shim.
|
||||
const pluginVersion = process.env.HYPERFRAMES_PLUGIN_VERSION;
|
||||
if (pluginVersion) {
|
||||
const invocation = resolveNpxInvocation(
|
||||
["--yes", `hyperframes@${pluginVersion}`, "media-use", "resolve", ...process.argv.slice(2)],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const result = spawnSync(invocation.cmd, invocation.args, invocation.opts);
|
||||
if (result.error) console.error(result.error.message);
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
const root = join(fileURLToPath(new URL("../../../", import.meta.url)));
|
||||
const dist = join(root, "packages/cli/dist/cli.js");
|
||||
const result = existsSync(dist)
|
||||
|
||||
Reference in New Issue
Block a user