mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 17:16:46 +03:00
Build: ship the launcher instead of bundling the JS engine
readSourceFiles no longer copies cli/engine into the skill; the scripts
payload is the launcher (executable bit preserved through dist, plugin/, and
universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every
build), the page JS, and command-metadata.json. Hook manifests call
`<scripts>/impeccable hook` behind an existence guard (Codex adds a
commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit;
GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and
systemMessage notice are gone. build:release fetches the pinned engine for
every target (lenient) and stages bin/<os-arch>/ into the dist skill copies
after root harness dirs and plugin/ were synced, so git-delivered trees stay
launcher-only. The detection-rule count check reads the vendored
extension/detector/antipatterns.json and is skipped when absent.
build:browser is a stub; the codex prefix rewrite leaves
`{{scripts_path}}/impeccable` alone.
Prepared with AI assistance (Claude Code).
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const BROWSER_DETECTOR_MODULES = [
|
||||
'cli/engine/shared/constants.mjs',
|
||||
'cli/engine/registry/antipatterns.mjs',
|
||||
'cli/engine/shared/color.mjs',
|
||||
'cli/engine/shared/fonts.mjs',
|
||||
'cli/engine/rules/checks.mjs',
|
||||
'cli/engine/browser/injected/index.mjs',
|
||||
];
|
||||
|
||||
function browserSafeModule(root, relPath) {
|
||||
let code = fs.readFileSync(path.join(root, relPath), 'utf-8');
|
||||
if (relPath === 'cli/engine/registry/antipatterns.mjs') {
|
||||
const match = code.match(/const ANTIPATTERNS = \[[\s\S]*?\n\];/);
|
||||
if (!match) throw new Error('Could not extract browser antipattern registry');
|
||||
code = match[0];
|
||||
}
|
||||
code = code.replace(/^import[\s\S]*?;\n/gm, '');
|
||||
code = code.replace(/^export\s+\{[^}]*\};\n?/gm, '');
|
||||
code = code.replace(/^export\s+\{[\s\S]*?^};\n?/gm, '');
|
||||
return `// --- ${relPath} ---\n${code.trim()}\n`;
|
||||
}
|
||||
|
||||
export function bundleBrowserDetectorModules(root) {
|
||||
return BROWSER_DETECTOR_MODULES
|
||||
.map((relPath) => browserSafeModule(root, relPath))
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {
|
||||
cleanDir,
|
||||
@@ -359,7 +360,12 @@ export function createTransformer(config) {
|
||||
ensureDir(scriptsOutDir);
|
||||
for (const script of skill.scripts) {
|
||||
const scriptContent = replaceScriptProviderMarker(script.content, placeholderKey, provider);
|
||||
writeFile(path.join(scriptsOutDir, script.name), scriptContent);
|
||||
const outPath = path.join(scriptsOutDir, script.name);
|
||||
writeFile(outPath, scriptContent);
|
||||
// The launcher must stay executable in every provider copy; a
|
||||
// plain write would drop the bit and `impeccable context` would
|
||||
// fail with EACCES on the first session.
|
||||
if (script.mode) fs.chmodSync(outPath, script.mode);
|
||||
scriptCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* correct wherever Claude Code unpacks the plugin.
|
||||
*/
|
||||
|
||||
export const IMPECCABLE_HOOK_COMMAND_MARKER = 'skills/impeccable/scripts/hook.mjs';
|
||||
export const IMPECCABLE_HOOK_COMMAND_MARKER = 'skills/impeccable/scripts/impeccable';
|
||||
|
||||
const TIMEOUT_SECONDS = 5;
|
||||
const STATUS_MESSAGE = 'Checking UI changes';
|
||||
@@ -34,12 +34,37 @@ const STATUS_MESSAGE = 'Checking UI changes';
|
||||
const STOP_TIMEOUT_SECONDS = 30;
|
||||
const STOP_STATUS_MESSAGE = 'Design deep pass';
|
||||
|
||||
function stopEntry(command) {
|
||||
// The hook is a verb of the impeccable launcher that ships in the skill's
|
||||
// scripts dir: `<scripts>/impeccable hook` (per-edit and Stop passes) and
|
||||
// `<scripts>/impeccable hook-before-edit` (Cursor's preToolUse). The launcher
|
||||
// runs the platform binary next to it, or downloads it once; no runtime probe
|
||||
// is needed and there is no Node on the path to check.
|
||||
export const LAUNCHER_NAME = 'impeccable';
|
||||
export const LAUNCHER_NAME_WINDOWS = 'impeccable.cmd';
|
||||
|
||||
// A hook manifest can be copied into a user-level settings file (issue #399:
|
||||
// user-level hooks fire in every project, where a project-relative path may
|
||||
// not exist). Guard the invocation so a missing launcher exits 0 without
|
||||
// swallowing the hook's real exit code when it is present: the `[ ! -f X ] ||
|
||||
// X verb` form (not `... || true`) preserves the launcher's exit code, so
|
||||
// Claude's exit-2 blocking signal still reaches the agent.
|
||||
export const guardedLauncher = (launcherPath, verb = 'hook') =>
|
||||
`[ ! -f "${launcherPath}" ] || "${launcherPath}" ${verb}`;
|
||||
|
||||
// cmd.exe form for harnesses that read a `commandWindows` sibling (Codex
|
||||
// 0.146.0+ selects it on Windows; issue #452). `exit /b` forwards the
|
||||
// launcher's errorlevel. Paths keep forward slashes; cmd.exe accepts them in
|
||||
// quoted paths and it is the form the CLI already writes.
|
||||
export const windowsLauncherCommand = (launcherCmdPath, verb = 'hook') =>
|
||||
`if exist "${launcherCmdPath}" ("${launcherCmdPath}" ${verb} & exit /b)`;
|
||||
|
||||
function stopEntry(command, commandWindows) {
|
||||
return {
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command,
|
||||
...(commandWindows ? { commandWindows } : {}),
|
||||
timeout: STOP_TIMEOUT_SECONDS,
|
||||
statusMessage: STOP_STATUS_MESSAGE,
|
||||
},
|
||||
@@ -47,50 +72,31 @@ function stopEntry(command) {
|
||||
};
|
||||
}
|
||||
|
||||
const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs';
|
||||
// The Node major the hook runtime requires, kept equal to the engines floor in
|
||||
// package.json. The probe and the notice both derive from it so they cannot
|
||||
// disagree about the supported version.
|
||||
const NODE_MAJOR_FLOOR = 22;
|
||||
// A hook manifest can be copied into a user-level settings file (issue #399:
|
||||
// user-level hooks fire in every project, where a project-relative path may
|
||||
// not exist). Guard node invocations so a missing file exits 0 without
|
||||
// swallowing node's real exit code when the file is present.
|
||||
//
|
||||
// The runtime is guarded too (issue #410): a `node` on PATH too old for the
|
||||
// hook's ESM syntax dies while hook.mjs is still being parsed, before the
|
||||
// script's own always-exit-0 contract can run, so the harness reported a hook
|
||||
// error on every edit and every Stop. Nothing written in ESM can report that
|
||||
// condition, so the command string itself checks the version floor first, in
|
||||
// ES5-only syntax that parses on any node old enough to fail it, and exits 0
|
||||
// when the runtime is unsupported or missing.
|
||||
//
|
||||
// `notice` reports the dead runtime to the user. It is passed per harness
|
||||
// because only some have a channel for it, checked against each harness's own
|
||||
// hook reference on the events we hook:
|
||||
// Claude Code / Codex: `systemMessage` on stdout is shown to the user -> notice
|
||||
// Cursor: preToolUse output is permission-shaped and its `user_message`
|
||||
// renders only on DENY, so warning would block the edit -> probe only
|
||||
// Grok Build: PostToolUse stdout is ignored; Stop additionalContext
|
||||
// reaches the model, but the node-version notice has no systemMessage
|
||||
// channel on this harness -> probe only
|
||||
// Copilot: output contract unconfirmed; do not guess a shape -> probe only
|
||||
//
|
||||
// The clamp avoids `<` and `>` deliberately: Volta's Windows shims run through
|
||||
// `cmd /C`, which reads an angle bracket in the `-e` payload as redirection, so
|
||||
// `>=` failed before node ran at all and the guard reported a missing runtime on
|
||||
// a machine that had a supported one (volta-cli/volta#1791). Newlines break the
|
||||
// same way, so this payload also has to stay on one line.
|
||||
const NODE_PROBE = `node -e "process.exit(Math.min(parseInt(process.versions.node,10),${NODE_MAJOR_FLOOR})===${NODE_MAJOR_FLOOR}?0:1)" 2>/dev/null`;
|
||||
const guardedNode = (hookPath, notice = '') => {
|
||||
const probe = notice
|
||||
? `! { ${NODE_PROBE} || { ${notice}; exit 0; }; }`
|
||||
: `! ${NODE_PROBE}`;
|
||||
return `[ ! -f "${hookPath}" ] || ${probe} || node "${hookPath}"`;
|
||||
};
|
||||
const launcherIn = (scriptsDir) => `${scriptsDir}/${LAUNCHER_NAME}`;
|
||||
const launcherCmdIn = (scriptsDir) => `${scriptsDir}/${LAUNCHER_NAME_WINDOWS}`;
|
||||
|
||||
function buildClaudeCompatibleHooks(matcher, hookPath, notice = '') {
|
||||
const command = guardedNode(hookPath, notice);
|
||||
const CLAUDE_PROJECT_SCRIPTS = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts';
|
||||
const CLAUDE_PLUGIN_SCRIPTS = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts';
|
||||
const CODEX_PLUGIN_SCRIPTS = '${PLUGIN_ROOT}/skills/impeccable/scripts';
|
||||
// Codex reads project hooks from `.codex/hooks.json`, but the skill payload the
|
||||
// hook invokes lives under the install's own skills dir: a `.codex`-directory
|
||||
// install keeps it at `.codex/skills/...`, while a `.agents` (Codex repo-skills)
|
||||
// install keeps it at `.agents/skills/...`. Derive the path from the install dir
|
||||
// so each generated manifest points at its own payload rather than a hardcoded
|
||||
// `.agents`; otherwise the guarded hook silently no-ops on `.codex` installs.
|
||||
const codexProjectScripts = (skillDir) => `${skillDir}/skills/impeccable/scripts`;
|
||||
const CURSOR_SCRIPTS = '.cursor/skills/impeccable/scripts';
|
||||
const GITHUB_PROJECT_SCRIPTS = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts';
|
||||
// Grok project hooks are relative to the git/workspace root. Claude tool names
|
||||
// in the matcher (Edit|Write|MultiEdit) alias to Grok's search_replace family.
|
||||
const GROK_PROJECT_SCRIPTS = '.grok/skills/impeccable/scripts';
|
||||
|
||||
// `windows: true` adds the `commandWindows` sibling; only Codex-shaped
|
||||
// consumers honor it, and an unknown key would fail Codex's strict parser if
|
||||
// it were the other way round, so it stays opt-in per manifest.
|
||||
function buildClaudeCompatibleHooks(matcher, scriptsDir, { windows = false } = {}) {
|
||||
const command = guardedLauncher(launcherIn(scriptsDir));
|
||||
const commandWindows = windows ? windowsLauncherCommand(launcherCmdIn(scriptsDir)) : undefined;
|
||||
return {
|
||||
PostToolUse: [
|
||||
{
|
||||
@@ -99,52 +105,21 @@ function buildClaudeCompatibleHooks(matcher, hookPath, notice = '') {
|
||||
{
|
||||
type: 'command',
|
||||
command,
|
||||
...(commandWindows ? { commandWindows } : {}),
|
||||
timeout: TIMEOUT_SECONDS,
|
||||
statusMessage: STATUS_MESSAGE,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
Stop: [stopEntry(command)],
|
||||
Stop: [stopEntry(command, commandWindows)],
|
||||
};
|
||||
}
|
||||
|
||||
// The message says `on PATH` deliberately: the common cause is a hook shell
|
||||
// whose PATH misses the version manager, so a user already running Node 22
|
||||
// needs to know the hook's PATH is at issue and not their install. Apostrophes
|
||||
// cannot appear in it, since it travels inside a single-quoted shell string.
|
||||
const NODE_NOTICE_TEXT = `The impeccable design hook is not running: no Node ${NODE_MAJOR_FLOOR} or newer on PATH. `
|
||||
+ 'Install one, or remove the impeccable hook from your harness settings.';
|
||||
// Claude Code and Codex both read `systemMessage`, so one payload serves both.
|
||||
// The marker under ~/.impeccable holds it to one notice per machine (not per
|
||||
// harness or per edit), and printf runs only after the marker write succeeds,
|
||||
// so an unwritable HOME degrades to silence rather than a notice on every edit.
|
||||
const SYSTEM_MESSAGE_NOTICE = 'D="$HOME/.impeccable"; [ -f "$D/node-unsupported" ] || '
|
||||
+ '{ mkdir -p "$D" 2>/dev/null && : > "$D/node-unsupported" 2>/dev/null && '
|
||||
+ `printf '%s' '{"systemMessage":"${NODE_NOTICE_TEXT}"}'; }`;
|
||||
const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
|
||||
const CODEX_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
|
||||
// Codex reads project hooks from `.codex/hooks.json`, but the skill payload the
|
||||
// hook invokes lives under the install's own skills dir: a `.codex`-directory
|
||||
// install keeps it at `.codex/skills/...`, while a `.agents` (Codex repo-skills)
|
||||
// install keeps it at `.agents/skills/...`. Derive the path from the install dir
|
||||
// so each generated manifest points at its own payload rather than a hardcoded
|
||||
// `.agents` — otherwise the guarded hook silently no-ops on `.codex` installs.
|
||||
const codexProjectHook = (skillDir) => `${skillDir}/skills/impeccable/scripts/hook.mjs`;
|
||||
const CURSOR_BEFORE_EDIT_SCRIPT = '.cursor/skills/impeccable/scripts/hook-before-edit.mjs';
|
||||
const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs';
|
||||
// Grok project hooks are relative to the git/workspace root. Claude tool names
|
||||
// in the matcher (Edit|Write|MultiEdit) alias to Grok's search_replace family.
|
||||
const GROK_PROJECT_HOOK = '.grok/skills/impeccable/scripts/hook.mjs';
|
||||
|
||||
export function buildClaudeSettingsManifest() {
|
||||
return {
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write',
|
||||
CLAUDE_PROJECT_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
hooks: buildClaudeCompatibleHooks('Edit|Write', CLAUDE_PROJECT_SCRIPTS),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,11 +131,7 @@ export function buildClaudeSettingsManifest() {
|
||||
// than `hooks`, failing the whole manifest (issue #330).
|
||||
export function buildClaudePluginHooksManifest() {
|
||||
return {
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write',
|
||||
CLAUDE_PLUGIN_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
hooks: buildClaudeCompatibleHooks('Edit|Write', CLAUDE_PLUGIN_SCRIPTS),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,11 +140,7 @@ export function buildClaudePluginHooksManifest() {
|
||||
// instead of relying on its Claude compatibility alias.
|
||||
export function buildCodexPluginHooksManifest() {
|
||||
return {
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write|apply_patch',
|
||||
CODEX_PLUGIN_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
hooks: buildClaudeCompatibleHooks('Edit|Write|apply_patch', CODEX_PLUGIN_SCRIPTS, { windows: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,13 +148,8 @@ export function buildCodexPluginHooksManifest() {
|
||||
// emitted command points at that install's payload. Defaults to `.codex` for the
|
||||
// Codex provider, whose self-consistent bundle keeps the skill at `.codex/skills`.
|
||||
export function buildCodexHooksManifest(skillDir = '.codex') {
|
||||
const hookPath = codexProjectHook(skillDir);
|
||||
return {
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write|apply_patch',
|
||||
hookPath,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
hooks: buildClaudeCompatibleHooks('Edit|Write|apply_patch', codexProjectScripts(skillDir), { windows: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,7 +159,7 @@ export function buildCursorHooksManifest() {
|
||||
hooks: {
|
||||
preToolUse: [
|
||||
{
|
||||
command: guardedNode(CURSOR_BEFORE_EDIT_SCRIPT),
|
||||
command: guardedLauncher(launcherIn(CURSOR_SCRIPTS), 'hook-before-edit'),
|
||||
timeout: TIMEOUT_SECONDS,
|
||||
},
|
||||
],
|
||||
@@ -224,7 +186,7 @@ export function buildGitHubHooksManifest() {
|
||||
{
|
||||
type: 'command',
|
||||
matcher: 'edit|create|apply_patch',
|
||||
bash: guardedNode(GITHUB_PROJECT_HOOK),
|
||||
bash: guardedLauncher(launcherIn(GITHUB_PROJECT_SCRIPTS)),
|
||||
timeoutSec: TIMEOUT_SECONDS,
|
||||
},
|
||||
],
|
||||
@@ -239,7 +201,7 @@ export function buildGitHubHooksManifest() {
|
||||
// https://docs.x.ai/build/features/hooks
|
||||
export function buildGrokHooksManifest() {
|
||||
return {
|
||||
hooks: buildClaudeCompatibleHooks('Edit|Write|MultiEdit', GROK_PROJECT_HOOK),
|
||||
hooks: buildClaudeCompatibleHooks('Edit|Write|MultiEdit', GROK_PROJECT_SCRIPTS),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+20
-61
@@ -9,18 +9,11 @@ import path from 'path';
|
||||
// New installs write project config at .impeccable/live/config.json instead.
|
||||
export const PER_PROJECT_SCRIPT_ARTIFACTS = new Set(['config.json']);
|
||||
|
||||
const DETECTOR_BUNDLE_DIR = 'cli/engine';
|
||||
|
||||
// Detector source files that live OUTSIDE `cli/engine` but are imported by the
|
||||
// bundled engine. `cli/engine/cli/main.mjs` imports `../../lib/impeccable-config.mjs`,
|
||||
// which in the source CLI resolves to `cli/lib/impeccable-config.mjs`. The detector
|
||||
// bundle copies `cli/engine/**` to `scripts/detector/**`, so from the bundled
|
||||
// `scripts/detector/cli/main.mjs` that same `../../lib/...` import resolves to
|
||||
// `scripts/lib/impeccable-config.mjs`. Copy the dependency there or the bundled
|
||||
// detector fails at import time with "Cannot find module .../lib/impeccable-config.mjs".
|
||||
const DETECTOR_EXTERNAL_DEPS = [
|
||||
{ src: 'cli/lib/impeccable-config.mjs', dest: 'lib/impeccable-config.mjs' },
|
||||
];
|
||||
// Platform binaries under `scripts/bin/<os>-<arch>/` are fetched per machine
|
||||
// (scripts/fetch-engine.mjs) and never part of the source skill read: the
|
||||
// release build stages them into provider output separately, and the launcher
|
||||
// downloads them on first run when they are absent.
|
||||
export const SKILL_BINARY_DIR = 'bin';
|
||||
|
||||
// Walk the harness-dir skill tree and return any per-project script
|
||||
// artifacts found, ready for restoration after a full sync rm+recopy.
|
||||
@@ -52,46 +45,6 @@ export function restorePerProjectArtifacts(rootDir, stashed) {
|
||||
}
|
||||
}
|
||||
|
||||
function readDetectorBundleScripts(rootDir) {
|
||||
const detectorDir = path.join(rootDir, DETECTOR_BUNDLE_DIR);
|
||||
if (!fs.existsSync(detectorDir)) return [];
|
||||
|
||||
const scripts = [];
|
||||
const walk = (dir) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(entryPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
const relPath = path.relative(detectorDir, entryPath).split(path.sep).join('/');
|
||||
scripts.push({
|
||||
name: `detector/${relPath}`,
|
||||
content: fs.readFileSync(entryPath, 'utf-8'),
|
||||
filePath: entryPath,
|
||||
generated: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
walk(detectorDir);
|
||||
|
||||
// Pull in engine dependencies that live outside the bundle dir so the
|
||||
// generated detector is self-contained (see DETECTOR_EXTERNAL_DEPS).
|
||||
for (const { src, dest } of DETECTOR_EXTERNAL_DEPS) {
|
||||
const srcPath = path.join(rootDir, src);
|
||||
if (!fs.existsSync(srcPath)) continue;
|
||||
scripts.push({
|
||||
name: dest,
|
||||
content: fs.readFileSync(srcPath, 'utf-8'),
|
||||
filePath: srcPath,
|
||||
generated: true,
|
||||
});
|
||||
}
|
||||
|
||||
return scripts;
|
||||
}
|
||||
|
||||
function readSkillScripts(scriptsDir) {
|
||||
const scripts = [];
|
||||
|
||||
@@ -102,6 +55,7 @@ function readSkillScripts(scriptsDir) {
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (dir === scriptsDir && entry.name === SKILL_BINARY_DIR) continue;
|
||||
walk(entryPath);
|
||||
continue;
|
||||
}
|
||||
@@ -109,10 +63,13 @@ function readSkillScripts(scriptsDir) {
|
||||
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(entry.name)) continue;
|
||||
|
||||
const relPath = path.relative(scriptsDir, entryPath).split(path.sep).join('/');
|
||||
// `mode` travels with the entry so the launcher keeps its executable
|
||||
// bit in every provider copy (see writeScriptFile in the transformer).
|
||||
scripts.push({
|
||||
name: relPath,
|
||||
content: fs.readFileSync(entryPath, 'utf-8'),
|
||||
filePath: entryPath,
|
||||
mode: fs.statSync(entryPath).mode & 0o777,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -242,8 +199,8 @@ export function readFilesRecursive(dir, fileList = []) {
|
||||
* The source manifest is `SKILL.src.md`, NOT `SKILL.md`, on purpose: the
|
||||
* `vercel-labs/skills` CLI discovers a skill by finding a literal `SKILL.md`
|
||||
* and copies that directory verbatim. If `skill/SKILL.md` existed, `npx skills`
|
||||
* would install the UNCOMPILED source (unresolved `{{placeholders}}`, no vendored
|
||||
* detector). Naming it `SKILL.src.md` hides it from discovery so the CLI falls
|
||||
* would install the UNCOMPILED source (unresolved `{{placeholders}}` and
|
||||
* `{{scripts_path}}`). Naming it `SKILL.src.md` hides it from discovery so the CLI falls
|
||||
* through to a compiled harness dir (`.agents/skills/impeccable`) instead.
|
||||
*/
|
||||
export function readSourceFiles(rootDir) {
|
||||
@@ -280,7 +237,6 @@ export function readSourceFiles(rootDir) {
|
||||
if (fs.existsSync(scriptsDir)) {
|
||||
scripts.push(...readSkillScripts(scriptsDir));
|
||||
}
|
||||
scripts.push(...readDetectorBundleScripts(rootDir));
|
||||
|
||||
const agents = [];
|
||||
const agentsDir = path.join(skillDir, 'agents');
|
||||
@@ -676,13 +632,15 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
|
||||
|
||||
// Replace `/skillname` invocations with the correct command prefix for this provider
|
||||
// (e.g., `/normalize` → `$normalize` for Codex). Require the slash to be
|
||||
// outside a path or URL so `.github/hooks/impeccable.json` and
|
||||
// `.codex/skills/impeccable` remain untouched.
|
||||
// outside a path or URL so `.github/hooks/impeccable.json`,
|
||||
// `.codex/skills/impeccable`, and the launcher invocation
|
||||
// `{{scripts_path}}/impeccable <verb>` (a `}}`-preceded path segment,
|
||||
// resolved after this pass) remain untouched.
|
||||
if (cmdPrefix !== '/' && allSkillNames.length > 0) {
|
||||
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
|
||||
for (const name of sorted) {
|
||||
result = result.replace(
|
||||
new RegExp(`(?<![a-zA-Z0-9_./-])\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
|
||||
new RegExp(`(?<![a-zA-Z0-9_./}>-])\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
|
||||
cmdPrefix
|
||||
);
|
||||
}
|
||||
@@ -695,9 +653,10 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
|
||||
* Render the one explicit provider marker allowed in executable skill scripts.
|
||||
*
|
||||
* Do not run replacePlaceholders() across JavaScript source: slash-command
|
||||
* heuristics can collide with regex literals and runtime paths. Scripts import
|
||||
* their command prefix from lib/provider.mjs, whose declaration is replaced
|
||||
* here by an exact string match.
|
||||
* heuristics can collide with regex literals and runtime paths. Only exact
|
||||
* marker lines are replaced. The shipped scripts today (the launcher and the
|
||||
* page JS) carry no marker; the binary derives its provider from its install
|
||||
* path or IMPECCABLE_PROVIDER_ID at run time.
|
||||
*/
|
||||
export function replaceScriptProviderMarker(content, provider, buildProvider = provider) {
|
||||
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor;
|
||||
|
||||
Reference in New Issue
Block a user