mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(cli): auto-copy all templates to dist and add skill lint (#153)
- Replace hardcoded template list in build:copy with `cp -r src/templates/*` so new templates are included automatically (kinetic-type, decision-tree, product-promo, nyt-graph were missing from published package) - Fix captions SKILL.md: reword `!` and `>` in inline backticks that triggered Claude Code's bash permission checker - Add scripts/lint-skills.ts to catch shell-unsafe patterns in SKILL.md files (runs as part of `bun run lint` in CI) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
25767fd8dd
commit
a9d49cd528
+2
-1
@@ -18,7 +18,8 @@
|
||||
"build:hyperframes-runtime:modular": "bun run --filter @hyperframes/core build:hyperframes-runtime:modular",
|
||||
"verify:packed-manifests": "node scripts/verify-packed-manifests.mjs",
|
||||
"set-version": "tsx scripts/set-version.ts",
|
||||
"lint": "oxlint .",
|
||||
"lint": "oxlint . && tsx scripts/lint-skills.ts",
|
||||
"lint:skills": "tsx scripts/lint-skills.ts",
|
||||
"lint:fix": "oxlint --fix .",
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"build:fonts": "cd ../producer && tsx scripts/generate-font-data.ts",
|
||||
"build:studio": "cd ../studio && bun run build",
|
||||
"build:runtime": "tsx scripts/build-runtime.ts",
|
||||
"build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/blank src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli src/templates/_shared dist/templates/ && cp -r ../../skills/hyperframes-compose ../../skills/hyperframes-captions dist/skills/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
|
||||
"build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/* dist/templates/ && cp -r ../../skills/hyperframes-compose ../../skills/hyperframes-captions dist/skills/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Lint SKILL.md files for patterns that break Claude Code's bash permission checker.
|
||||
*
|
||||
* Claude Code scans skill content for shell-like patterns. Inline backtick code
|
||||
* containing `!` (history expansion) or `>` (output redirection) outside of fenced
|
||||
* code blocks triggers false positives and prevents the skill from loading.
|
||||
*
|
||||
* Safe: fenced code blocks (```...```), HTML tags in backticks (`<div>`)
|
||||
* Unsafe: `!` followed by `>` later in the same text block
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const SKILLS_DIR = join(import.meta.dirname, "..", "skills");
|
||||
|
||||
interface Violation {
|
||||
file: string;
|
||||
line: number;
|
||||
message: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// Patterns that trigger Claude Code's bash permission checker when found in
|
||||
// inline backtick spans (not fenced code blocks).
|
||||
// - Backtick-wrapped `!` — interpreted as bash history expansion
|
||||
// - Bare `>` outside fenced blocks when preceded by `!` — interpreted as redirection
|
||||
const DANGEROUS_INLINE_PATTERNS: { pattern: RegExp; message: string }[] = [
|
||||
{
|
||||
// `!` in backticks triggers bash history expansion detection, which then
|
||||
// causes Claude Code to scan surrounding text for `>` (redirection).
|
||||
pattern: /`[^`]*![^`]*`/,
|
||||
message:
|
||||
'Inline backtick contains `!` — Claude Code interprets this as bash history expansion. Use the word instead (e.g., "exclamation").',
|
||||
},
|
||||
{
|
||||
// Bare `>` followed by a word char (e.g., `>file`, `>150ms`) looks like
|
||||
// output redirection. HTML tag closers (`<div>`, `</script>`) are fine
|
||||
// because `>` is followed by `<`, space, backtick, or end of string.
|
||||
pattern: /`[^`]*>\w[^`]*`/,
|
||||
message:
|
||||
'Inline backtick contains `>` followed by a word character — Claude Code may interpret this as output redirection. Rephrase (e.g., "150ms+" instead of ">150ms").',
|
||||
},
|
||||
];
|
||||
|
||||
function collectSkillFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectSkillFiles(full));
|
||||
} else if (entry.name === "SKILL.md") {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/** Strip fenced code blocks so we only lint prose + inline code. */
|
||||
function stripFencedBlocks(content: string): string {
|
||||
return content.replace(/^```[\s\S]*?^```/gm, (match) =>
|
||||
match
|
||||
.split("\n")
|
||||
.map(() => "")
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
function lintFile(filePath: string): Violation[] {
|
||||
const raw = readFileSync(filePath, "utf-8");
|
||||
const stripped = stripFencedBlocks(raw);
|
||||
const lines = stripped.split("\n");
|
||||
const violations: Violation[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line) continue;
|
||||
|
||||
for (const { pattern, message } of DANGEROUS_INLINE_PATTERNS) {
|
||||
if (pattern.test(line)) {
|
||||
violations.push({
|
||||
file: relative(process.cwd(), filePath),
|
||||
line: i + 1,
|
||||
message,
|
||||
text: line.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (!statSync(SKILLS_DIR, { throwIfNoEntry: false })?.isDirectory()) {
|
||||
console.log("No skills/ directory found — skipping skill lint.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const files = collectSkillFiles(SKILLS_DIR);
|
||||
if (files.length === 0) {
|
||||
console.log("No SKILL.md files found.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let totalViolations = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const violations = lintFile(file);
|
||||
for (const v of violations) {
|
||||
console.error(`${v.file}:${v.line}: ${v.message}`);
|
||||
console.error(` ${v.text}\n`);
|
||||
totalViolations++;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalViolations > 0) {
|
||||
console.error(`\n${totalViolations} skill lint error(s) found.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`Checked ${files.length} skill file(s) — no issues found.`);
|
||||
}
|
||||
@@ -193,7 +193,7 @@ Group size affects pacing. Fast content needs fast caption turnover.
|
||||
- **Conversational:** 3-5 words per group. Natural phrase length.
|
||||
- **Measured/calm:** 4-6 words per group. Longer groups match slower pace.
|
||||
|
||||
Break groups on sentence boundaries (`.` `?` `!`), pauses (>150ms gap), or max word count — whichever comes first.
|
||||
Break groups on sentence boundaries (period, question mark, exclamation), pauses (150ms+ gap), or max word count — whichever comes first.
|
||||
|
||||
## Positioning
|
||||
|
||||
|
||||
Reference in New Issue
Block a user