mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
611147a333 | ||
|
|
7d5c60d291 |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"description": "Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.",
|
||||
"description": "Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -137,9 +137,9 @@ const GROK_PROJECT_HOOK = '.grok/skills/impeccable/scripts/hook.mjs';
|
||||
|
||||
export function buildClaudeSettingsManifest() {
|
||||
return {
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write|MultiEdit',
|
||||
'Edit|Write',
|
||||
CLAUDE_PROJECT_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
@@ -155,7 +155,7 @@ export function buildClaudeSettingsManifest() {
|
||||
export function buildClaudePluginHooksManifest() {
|
||||
return {
|
||||
hooks: buildClaudeCompatibleHooks(
|
||||
'Edit|Write|MultiEdit',
|
||||
'Edit|Write',
|
||||
CLAUDE_PLUGIN_HOOK,
|
||||
SYSTEM_MESSAGE_NOTICE,
|
||||
),
|
||||
|
||||
@@ -44,7 +44,7 @@ The first argument is the action. Defaults to `status`.
|
||||
```
|
||||
|
||||
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `{{command_prefix}}impeccable hooks on`."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."
|
||||
4. If `<action>` is `on`, follow up with: "Done. The design hook will fire after the next Edit/Write on a UI file."
|
||||
5. If `<action>` is `ignore-value`, `ignore-file`, or `ignore-rule`, just print the script output. The default scope is shared `.impeccable/config.json`; add `--local` only when the user explicitly asks for a private exception.
|
||||
6. If `<action>` is `status`, just print the script output. Do not add commentary unless the user asked a follow-up question.
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ const HOOK_MANIFEST_TARGETS = [
|
||||
destRel: '.claude/settings.local.json',
|
||||
sharedDestRel: '.claude/settings.json',
|
||||
manifest: () => ({
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
||||
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
|
||||
hooks: {
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: 'Edit|Write|MultiEdit',
|
||||
matcher: 'Edit|Write',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
|
||||
@@ -11,8 +11,6 @@ import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { firstExistingFile, hasAnyDependency } from './frameworks/detect-utils.mjs';
|
||||
|
||||
export const SVELTE_LIVE_ROOT_COMPONENT = 'src/lib/impeccable/ImpeccableLiveRoot.svelte';
|
||||
export const SVELTE_LAYOUT_MARKER_OPEN = '<!-- impeccable-live-svelte-start -->';
|
||||
export const SVELTE_LAYOUT_MARKER_CLOSE = '<!-- impeccable-live-svelte-end -->';
|
||||
@@ -47,17 +45,11 @@ export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
|
||||
&& fileIncludes(path.join(cwd, appHtml), '%sveltekit.head%');
|
||||
if (!hasTemplateMarkers) return null;
|
||||
|
||||
const hasSvelteConfig = Boolean(firstExistingFile(cwd, [
|
||||
'svelte.config.js',
|
||||
'svelte.config.mjs',
|
||||
'svelte.config.cjs',
|
||||
'svelte.config.ts',
|
||||
]));
|
||||
const hasKitPackage = hasAnyDependency(cwd, [
|
||||
'@sveltejs/kit',
|
||||
'@sveltejs/vite-plugin-svelte',
|
||||
'svelte',
|
||||
]);
|
||||
const hasSvelteConfig = fs.existsSync(path.join(cwd, 'svelte.config.js'))
|
||||
|| fs.existsSync(path.join(cwd, 'svelte.config.mjs'))
|
||||
|| fs.existsSync(path.join(cwd, 'svelte.config.cjs'))
|
||||
|| fs.existsSync(path.join(cwd, 'svelte.config.ts'));
|
||||
const hasKitPackage = packageHasSvelteKit(cwd);
|
||||
if (!hasSvelteConfig && !hasKitPackage) return null;
|
||||
|
||||
return {
|
||||
@@ -268,16 +260,36 @@ function findSvelteKitAppHtml(cwd, config) {
|
||||
}
|
||||
|
||||
function findSvelteKitLayout(cwd) {
|
||||
return firstExistingFile(cwd, [
|
||||
const candidates = [
|
||||
'src/routes/+layout.svelte',
|
||||
'src/routes/(app)/+layout.svelte',
|
||||
]) || 'src/routes/+layout.svelte';
|
||||
];
|
||||
for (const rel of candidates) {
|
||||
if (fs.existsSync(path.join(cwd, rel))) return rel;
|
||||
}
|
||||
return 'src/routes/+layout.svelte';
|
||||
}
|
||||
|
||||
function defaultSvelteLayout() {
|
||||
return `<script>\n let { children } = $props();\n</script>\n\n{@render children?.()}\n`;
|
||||
}
|
||||
|
||||
function packageHasSvelteKit(cwd) {
|
||||
const file = path.join(cwd, 'package.json');
|
||||
if (!fs.existsSync(file)) return false;
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
const deps = {
|
||||
...(pkg.dependencies || {}),
|
||||
...(pkg.devDependencies || {}),
|
||||
...(pkg.peerDependencies || {}),
|
||||
};
|
||||
return Boolean(deps['@sveltejs/kit'] || deps['@sveltejs/vite-plugin-svelte'] || deps.svelte);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fileIncludes(file, text) {
|
||||
try {
|
||||
return fs.readFileSync(file, 'utf-8').includes(text);
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { firstExistingFile, hasAnyDependency } from './frameworks/detect-utils.mjs';
|
||||
import { buildLiveScriptSrc } from './frameworks/script-src.mjs';
|
||||
|
||||
export const TANSTACK_MARKER_OPEN = '{/* impeccable-live-tanstack-start */}';
|
||||
@@ -44,8 +42,8 @@ const START_PACKAGES = [
|
||||
];
|
||||
|
||||
export function detectTanStackStartProject(cwd = process.cwd()) {
|
||||
if (!hasAnyDependency(cwd, START_PACKAGES)) return null;
|
||||
const rootRoute = firstExistingFile(cwd, ROOT_ROUTE_CANDIDATES);
|
||||
if (!packageHasTanStackStart(cwd)) return null;
|
||||
const rootRoute = findRootRouteFile(cwd);
|
||||
if (!rootRoute) return null;
|
||||
|
||||
const ext = path.extname(rootRoute);
|
||||
@@ -220,6 +218,29 @@ function isManagedComponent(content) {
|
||||
return String(content || '').includes('impeccable-live-tanstack');
|
||||
}
|
||||
|
||||
function findRootRouteFile(cwd) {
|
||||
for (const rel of ROOT_ROUTE_CANDIDATES) {
|
||||
if (fs.existsSync(path.join(cwd, rel))) return rel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function packageHasTanStackStart(cwd) {
|
||||
const file = path.join(cwd, 'package.json');
|
||||
if (!fs.existsSync(file)) return false;
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
const deps = {
|
||||
...(pkg.dependencies || {}),
|
||||
...(pkg.devDependencies || {}),
|
||||
...(pkg.peerDependencies || {}),
|
||||
};
|
||||
return START_PACKAGES.some((name) => Boolean(deps[name]));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function relativeImportSpecifier(fromFile, toFile) {
|
||||
const rel = path.posix.relative(
|
||||
path.posix.dirname(fromFile.split(path.sep).join('/')),
|
||||
|
||||
@@ -72,7 +72,8 @@ describe('hook manifest builders', () => {
|
||||
const group = manifest.hooks.PostToolUse[0];
|
||||
const handler = group.hooks[0];
|
||||
|
||||
assert.equal(group.matcher, 'Edit|Write|MultiEdit');
|
||||
assert.equal(group.matcher, 'Edit|Write');
|
||||
assert.doesNotMatch(manifest.description, /MultiEdit/);
|
||||
assert.equal(handler.type, 'command');
|
||||
assert.equal(handler.timeout, 5);
|
||||
assert.equal(handler.statusMessage, 'Checking UI changes');
|
||||
@@ -356,7 +357,7 @@ describe('generated hook artifacts in repo', () => {
|
||||
assert.equal(manifest.description, undefined);
|
||||
|
||||
const handler = manifest.hooks.PostToolUse[0].hooks[0];
|
||||
assert.equal(manifest.hooks.PostToolUse[0].matcher, 'Edit|Write|MultiEdit');
|
||||
assert.equal(manifest.hooks.PostToolUse[0].matcher, 'Edit|Write');
|
||||
expectCommand(handler.command, 'skills/impeccable/scripts/hook.mjs');
|
||||
// Resolves relative to the installed plugin, not a `.claude/skills/` layout.
|
||||
assert.ok(handler.command.includes('${CLAUDE_PLUGIN_ROOT}'),
|
||||
@@ -375,6 +376,15 @@ describe('generated hook artifacts in repo', () => {
|
||||
assert.ok(fs.existsSync(path.join(REPO_ROOT, 'plugin/skills/impeccable/scripts/hook-lib.mjs')));
|
||||
});
|
||||
|
||||
it('keeps the marketplace hook repair matcher aligned with Claude Code', () => {
|
||||
const hookAdmin = fs.readFileSync(
|
||||
path.join(REPO_ROOT, 'plugin/skills/impeccable/scripts/hook-admin.mjs'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(hookAdmin, /matcher: 'Edit\|Write'/);
|
||||
assert.doesNotMatch(hookAdmin, /matcher: 'Edit\|Write\|MultiEdit'/);
|
||||
});
|
||||
|
||||
it('generated hook runtime can import the bundled detector', async () => {
|
||||
for (const scriptDir of [
|
||||
'.claude/skills/impeccable/scripts',
|
||||
|
||||
@@ -947,6 +947,11 @@ describe('hook-admin.mjs', () => {
|
||||
// impeccable entry must have been stripped, not accumulated.
|
||||
assert.equal(claude.split('skills/impeccable/scripts/hook.mjs').length - 1, 2);
|
||||
assert.match(claude, /"Stop"/);
|
||||
const claudeManifest = JSON.parse(claude);
|
||||
const impeccableGroup = claudeManifest.hooks.PostToolUse.find((group) =>
|
||||
group.hooks?.some((hook) => hook.command?.includes('skills/impeccable/scripts/hook.mjs')));
|
||||
assert.ok(impeccableGroup, 'repaired Claude settings should contain the Impeccable PostToolUse group');
|
||||
assert.equal(impeccableGroup.matcher, 'Edit|Write');
|
||||
|
||||
const codex = fs.readFileSync(path.join(cwd, '.codex', 'hooks.json'), 'utf-8');
|
||||
assert.match(codex, /\.agents\/skills\/impeccable\/scripts\/hook\.mjs/);
|
||||
|
||||
Reference in New Issue
Block a user