@soleri/cli 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.js +39 -4
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/hooks.js +70 -1
- package/dist/commands/hooks.js.map +1 -1
- package/dist/hook-packs/a11y/hookify.focus-ring-required.local.md +20 -0
- package/dist/hook-packs/a11y/hookify.semantic-html.local.md +17 -0
- package/dist/hook-packs/a11y/hookify.ux-touch-targets.local.md +17 -0
- package/dist/hook-packs/a11y/manifest.json +5 -0
- package/dist/hook-packs/clean-commits/hookify.no-ai-attribution.local.md +17 -0
- package/dist/hook-packs/clean-commits/manifest.json +5 -0
- package/dist/hook-packs/css-discipline/hookify.no-important.local.md +17 -0
- package/dist/hook-packs/css-discipline/hookify.no-inline-styles.local.md +20 -0
- package/dist/hook-packs/css-discipline/manifest.json +5 -0
- package/dist/hook-packs/full/manifest.json +6 -0
- package/dist/hook-packs/installer.d.ts +19 -0
- package/dist/hook-packs/installer.js +103 -0
- package/dist/hook-packs/installer.js.map +1 -0
- package/dist/hook-packs/installer.ts +113 -0
- package/dist/hook-packs/registry.d.ts +21 -0
- package/dist/hook-packs/registry.js +69 -0
- package/dist/hook-packs/registry.js.map +1 -0
- package/dist/hook-packs/registry.ts +84 -0
- package/dist/hook-packs/typescript-safety/hookify.no-any-types.local.md +17 -0
- package/dist/hook-packs/typescript-safety/hookify.no-console-log.local.md +20 -0
- package/dist/hook-packs/typescript-safety/manifest.json +5 -0
- package/package.json +2 -2
- package/src/__tests__/hook-packs.test.ts +195 -0
- package/src/commands/create.ts +43 -3
- package/src/commands/hooks.ts +75 -1
- package/src/hook-packs/a11y/hookify.focus-ring-required.local.md +20 -0
- package/src/hook-packs/a11y/hookify.semantic-html.local.md +17 -0
- package/src/hook-packs/a11y/hookify.ux-touch-targets.local.md +17 -0
- package/src/hook-packs/a11y/manifest.json +5 -0
- package/src/hook-packs/clean-commits/hookify.no-ai-attribution.local.md +17 -0
- package/src/hook-packs/clean-commits/manifest.json +5 -0
- package/src/hook-packs/css-discipline/hookify.no-important.local.md +17 -0
- package/src/hook-packs/css-discipline/hookify.no-inline-styles.local.md +20 -0
- package/src/hook-packs/css-discipline/manifest.json +5 -0
- package/src/hook-packs/full/manifest.json +6 -0
- package/src/hook-packs/installer.ts +113 -0
- package/src/hook-packs/registry.ts +84 -0
- package/src/hook-packs/typescript-safety/hookify.no-any-types.local.md +17 -0
- package/src/hook-packs/typescript-safety/hookify.no-console-log.local.md +20 -0
- package/src/hook-packs/typescript-safety/manifest.json +5 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Soleri Hook Pack: css-discipline
|
|
3
|
+
# Rule: no-important
|
|
4
|
+
name: no-important
|
|
5
|
+
enabled: true
|
|
6
|
+
event: file
|
|
7
|
+
action: block
|
|
8
|
+
conditions:
|
|
9
|
+
- field: file_path
|
|
10
|
+
operator: regex_match
|
|
11
|
+
pattern: \.(tsx?|jsx?|css)$
|
|
12
|
+
- field: content
|
|
13
|
+
operator: regex_match
|
|
14
|
+
pattern: "!important"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
🚫 **!important blocked.** Fix specificity instead. Use Tailwind `!` prefix (`!text-error`) only if needed.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Soleri Hook Pack: css-discipline
|
|
3
|
+
# Rule: no-inline-styles
|
|
4
|
+
name: no-inline-styles
|
|
5
|
+
enabled: true
|
|
6
|
+
event: file
|
|
7
|
+
action: warn
|
|
8
|
+
conditions:
|
|
9
|
+
- field: file_path
|
|
10
|
+
operator: regex_match
|
|
11
|
+
pattern: src/components/.*\.tsx$
|
|
12
|
+
- field: file_path
|
|
13
|
+
operator: not_contains
|
|
14
|
+
pattern: .stories.tsx
|
|
15
|
+
- field: content
|
|
16
|
+
operator: regex_match
|
|
17
|
+
pattern: style=\{\{[^}]*(padding|margin|width|height|fontSize|color|background)[^}]*\}\}
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
⚠️ **Inline style detected.** Use Tailwind: `p-4` not `style={{ padding: '16px' }}`. Exception: CSS variables.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "full",
|
|
3
|
+
"description": "Complete quality suite — all 8 hooks",
|
|
4
|
+
"hooks": ["no-any-types", "no-console-log", "no-important", "no-inline-styles", "semantic-html", "focus-ring-required", "ux-touch-targets", "no-ai-attribution"],
|
|
5
|
+
"composedFrom": ["typescript-safety", "a11y", "css-discipline", "clean-commits"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook pack installer — copies hookify files to ~/.claude/ for global enforcement.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, copyFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { getPack } from './registry.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve all hookify file paths for a pack, handling composed packs.
|
|
11
|
+
* Returns a map of hook name → source file path.
|
|
12
|
+
*/
|
|
13
|
+
function resolveHookFiles(packName: string): Map<string, string> {
|
|
14
|
+
const pack = getPack(packName);
|
|
15
|
+
if (!pack) return new Map();
|
|
16
|
+
|
|
17
|
+
const files = new Map<string, string>();
|
|
18
|
+
|
|
19
|
+
if (pack.manifest.composedFrom) {
|
|
20
|
+
// Composed pack: gather files from constituent packs
|
|
21
|
+
for (const subPackName of pack.manifest.composedFrom) {
|
|
22
|
+
const subFiles = resolveHookFiles(subPackName);
|
|
23
|
+
for (const [hook, path] of subFiles) {
|
|
24
|
+
files.set(hook, path);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
// Direct pack: look for hookify files in the pack directory
|
|
29
|
+
for (const hook of pack.manifest.hooks) {
|
|
30
|
+
const filePath = join(pack.dir, `hookify.${hook}.local.md`);
|
|
31
|
+
if (existsSync(filePath)) {
|
|
32
|
+
files.set(hook, filePath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Install a hook pack globally to ~/.claude/.
|
|
42
|
+
* Skips files that already exist (idempotent).
|
|
43
|
+
*/
|
|
44
|
+
export function installPack(packName: string): { installed: string[]; skipped: string[] } {
|
|
45
|
+
const pack = getPack(packName);
|
|
46
|
+
if (!pack) {
|
|
47
|
+
throw new Error(`Unknown hook pack: "${packName}"`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const claudeDir = join(homedir(), '.claude');
|
|
51
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
const hookFiles = resolveHookFiles(packName);
|
|
54
|
+
const installed: string[] = [];
|
|
55
|
+
const skipped: string[] = [];
|
|
56
|
+
|
|
57
|
+
for (const [hook, sourcePath] of hookFiles) {
|
|
58
|
+
const destPath = join(claudeDir, `hookify.${hook}.local.md`);
|
|
59
|
+
if (existsSync(destPath)) {
|
|
60
|
+
skipped.push(hook);
|
|
61
|
+
} else {
|
|
62
|
+
copyFileSync(sourcePath, destPath);
|
|
63
|
+
installed.push(hook);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { installed, skipped };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove a hook pack's files from ~/.claude/.
|
|
72
|
+
*/
|
|
73
|
+
export function removePack(packName: string): { removed: string[] } {
|
|
74
|
+
const pack = getPack(packName);
|
|
75
|
+
if (!pack) {
|
|
76
|
+
throw new Error(`Unknown hook pack: "${packName}"`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const claudeDir = join(homedir(), '.claude');
|
|
80
|
+
const removed: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (const hook of pack.manifest.hooks) {
|
|
83
|
+
const filePath = join(claudeDir, `hookify.${hook}.local.md`);
|
|
84
|
+
if (existsSync(filePath)) {
|
|
85
|
+
unlinkSync(filePath);
|
|
86
|
+
removed.push(hook);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { removed };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a pack is installed.
|
|
95
|
+
* Returns true (all hooks present), false (none present), or 'partial'.
|
|
96
|
+
*/
|
|
97
|
+
export function isPackInstalled(packName: string): boolean | 'partial' {
|
|
98
|
+
const pack = getPack(packName);
|
|
99
|
+
if (!pack) return false;
|
|
100
|
+
|
|
101
|
+
const claudeDir = join(homedir(), '.claude');
|
|
102
|
+
let present = 0;
|
|
103
|
+
|
|
104
|
+
for (const hook of pack.manifest.hooks) {
|
|
105
|
+
if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
|
|
106
|
+
present++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (present === 0) return false;
|
|
111
|
+
if (present === pack.manifest.hooks.length) return true;
|
|
112
|
+
return 'partial';
|
|
113
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook pack registry — discovers built-in packs and checks installation status.
|
|
3
|
+
*/
|
|
4
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
export interface HookPackManifest {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
hooks: string[];
|
|
13
|
+
composedFrom?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
/** Root directory containing all built-in hook packs. */
|
|
20
|
+
function getPacksRoot(): string {
|
|
21
|
+
return __dirname;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* List all available built-in hook packs.
|
|
26
|
+
*/
|
|
27
|
+
export function listPacks(): HookPackManifest[] {
|
|
28
|
+
const root = getPacksRoot();
|
|
29
|
+
const entries = readdirSync(root, { withFileTypes: true });
|
|
30
|
+
const packs: HookPackManifest[] = [];
|
|
31
|
+
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!entry.isDirectory()) continue;
|
|
34
|
+
const manifestPath = join(root, entry.name, 'manifest.json');
|
|
35
|
+
if (!existsSync(manifestPath)) continue;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as HookPackManifest;
|
|
39
|
+
packs.push(manifest);
|
|
40
|
+
} catch {
|
|
41
|
+
// Skip malformed manifests
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return packs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a specific pack by name.
|
|
50
|
+
*/
|
|
51
|
+
export function getPack(name: string): { manifest: HookPackManifest; dir: string } | null {
|
|
52
|
+
const root = getPacksRoot();
|
|
53
|
+
const dir = join(root, name);
|
|
54
|
+
const manifestPath = join(dir, 'manifest.json');
|
|
55
|
+
|
|
56
|
+
if (!existsSync(manifestPath)) return null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as HookPackManifest;
|
|
60
|
+
return { manifest, dir };
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get names of packs that are fully installed in ~/.claude/.
|
|
68
|
+
*/
|
|
69
|
+
export function getInstalledPacks(): string[] {
|
|
70
|
+
const claudeDir = join(homedir(), '.claude');
|
|
71
|
+
const packs = listPacks();
|
|
72
|
+
const installed: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const pack of packs) {
|
|
75
|
+
const allPresent = pack.hooks.every((hook) =>
|
|
76
|
+
existsSync(join(claudeDir, `hookify.${hook}.local.md`)),
|
|
77
|
+
);
|
|
78
|
+
if (allPresent) {
|
|
79
|
+
installed.push(pack.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return installed;
|
|
84
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Soleri Hook Pack: typescript-safety
|
|
3
|
+
# Rule: no-any-types
|
|
4
|
+
name: no-any-types
|
|
5
|
+
enabled: true
|
|
6
|
+
event: file
|
|
7
|
+
action: block
|
|
8
|
+
conditions:
|
|
9
|
+
- field: file_path
|
|
10
|
+
operator: regex_match
|
|
11
|
+
pattern: \.(tsx?|jsx?)$
|
|
12
|
+
- field: content
|
|
13
|
+
operator: regex_match
|
|
14
|
+
pattern: (:\s*any(?![a-zA-Z])|as\s+any(?![a-zA-Z])|<any>|Record<string,\s*any>)
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
🚫 **Type bypass blocked.** Replace `:any` with specific type or `unknown`. Fix `as any` at the source.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Soleri Hook Pack: typescript-safety
|
|
3
|
+
# Rule: no-console-log
|
|
4
|
+
name: no-console-log
|
|
5
|
+
enabled: true
|
|
6
|
+
event: file
|
|
7
|
+
action: warn
|
|
8
|
+
conditions:
|
|
9
|
+
- field: file_path
|
|
10
|
+
operator: regex_match
|
|
11
|
+
pattern: src/.*\.(tsx?|jsx?)$
|
|
12
|
+
- field: file_path
|
|
13
|
+
operator: not_regex_match
|
|
14
|
+
pattern: (\.test\.|\.spec\.|\.stories\.)
|
|
15
|
+
- field: content
|
|
16
|
+
operator: regex_match
|
|
17
|
+
pattern: console\.(log|debug|info)\(
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
⚠️ **Debug code detected.** Remove `console.log/debug/info` before commit. `console.error/warn` allowed.
|