@hasna/hooks 0.0.1
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/.npmrc.example +2 -0
- package/AGENTS.md +54 -0
- package/CLAUDE.md +70 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +232 -0
- package/bin/index.js +5171 -0
- package/hooks/hook-agentmessages/CLAUDE.md +79 -0
- package/hooks/hook-agentmessages/LICENSE +21 -0
- package/hooks/hook-agentmessages/README.md +107 -0
- package/hooks/hook-agentmessages/package.json +31 -0
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/CLAUDE.md +23 -0
- package/hooks/hook-branchprotect/README.md +25 -0
- package/hooks/hook-branchprotect/package.json +42 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-branchprotect/tsconfig.json +25 -0
- package/hooks/hook-checkbugs/LICENSE +21 -0
- package/hooks/hook-checkbugs/README.md +140 -0
- package/hooks/hook-checkbugs/package.json +58 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkbugs/tsconfig.json +15 -0
- package/hooks/hook-checkdocs/README.md +137 -0
- package/hooks/hook-checkdocs/package.json +57 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkdocs/tsconfig.json +15 -0
- package/hooks/hook-checkfiles/LICENSE +21 -0
- package/hooks/hook-checkfiles/README.md +141 -0
- package/hooks/hook-checkfiles/package.json +56 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checkfiles/tsconfig.json +15 -0
- package/hooks/hook-checklint/LICENSE +21 -0
- package/hooks/hook-checklint/README.md +147 -0
- package/hooks/hook-checklint/package.json +57 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checklint/tsconfig.json +15 -0
- package/hooks/hook-checkpoint/CLAUDE.md +23 -0
- package/hooks/hook-checkpoint/README.md +37 -0
- package/hooks/hook-checkpoint/package.json +58 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checkpoint/tsconfig.json +25 -0
- package/hooks/hook-checksecurity/LICENSE +21 -0
- package/hooks/hook-checksecurity/README.md +158 -0
- package/hooks/hook-checksecurity/package.json +57 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checksecurity/tsconfig.json +15 -0
- package/hooks/hook-checktasks/README.md +144 -0
- package/hooks/hook-checktasks/package.json +55 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktasks/tsconfig.json +20 -0
- package/hooks/hook-checktests/LICENSE +21 -0
- package/hooks/hook-checktests/README.md +137 -0
- package/hooks/hook-checktests/package.json +57 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-checktests/tsconfig.json +15 -0
- package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
- package/hooks/hook-contextrefresh/README.md +42 -0
- package/hooks/hook-contextrefresh/package.json +42 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-contextrefresh/tsconfig.json +25 -0
- package/hooks/hook-gitguard/CLAUDE.md +22 -0
- package/hooks/hook-gitguard/README.md +30 -0
- package/hooks/hook-gitguard/package.json +57 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-gitguard/tsconfig.json +25 -0
- package/hooks/hook-packageage/CLAUDE.md +23 -0
- package/hooks/hook-packageage/README.md +33 -0
- package/hooks/hook-packageage/package.json +42 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-packageage/tsconfig.json +25 -0
- package/hooks/hook-phonenotify/CLAUDE.md +25 -0
- package/hooks/hook-phonenotify/README.md +44 -0
- package/hooks/hook-phonenotify/package.json +42 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-phonenotify/tsconfig.json +25 -0
- package/hooks/hook-precompact/CLAUDE.md +23 -0
- package/hooks/hook-precompact/README.md +36 -0
- package/hooks/hook-precompact/package.json +42 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/hooks/hook-precompact/tsconfig.json +25 -0
- package/package.json +61 -0
- package/src/cli/components/App.tsx +191 -0
- package/src/cli/components/CategorySelect.tsx +37 -0
- package/src/cli/components/DataTable.tsx +133 -0
- package/src/cli/components/Header.tsx +18 -0
- package/src/cli/components/HookSelect.tsx +29 -0
- package/src/cli/components/InstallProgress.tsx +105 -0
- package/src/cli/components/SearchView.tsx +86 -0
- package/src/cli/index.tsx +218 -0
- package/src/index.ts +31 -0
- package/src/lib/installer.ts +288 -0
- package/src/lib/registry.ts +205 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import SelectInput from "ink-select-input";
|
|
4
|
+
import { Header } from "./Header.js";
|
|
5
|
+
import { CategorySelect } from "./CategorySelect.js";
|
|
6
|
+
import { HookSelect } from "./HookSelect.js";
|
|
7
|
+
import { SearchView } from "./SearchView.js";
|
|
8
|
+
import { InstallProgress } from "./InstallProgress.js";
|
|
9
|
+
import {
|
|
10
|
+
getHooksByCategory,
|
|
11
|
+
HookMeta,
|
|
12
|
+
Category,
|
|
13
|
+
} from "../../lib/registry.js";
|
|
14
|
+
import { InstallResult } from "../../lib/installer.js";
|
|
15
|
+
|
|
16
|
+
type View = "main" | "browse" | "search" | "hooks" | "installing" | "done";
|
|
17
|
+
|
|
18
|
+
interface AppProps {
|
|
19
|
+
initialHooks?: string[];
|
|
20
|
+
overwrite?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function App({ initialHooks, overwrite = false }: AppProps) {
|
|
24
|
+
const { exit } = useApp();
|
|
25
|
+
const [view, setView] = useState<View>(
|
|
26
|
+
initialHooks?.length ? "installing" : "main"
|
|
27
|
+
);
|
|
28
|
+
const [category, setCategory] = useState<Category | null>(null);
|
|
29
|
+
const [selected, setSelected] = useState<Set<string>>(
|
|
30
|
+
new Set(initialHooks || [])
|
|
31
|
+
);
|
|
32
|
+
const [results, setResults] = useState<InstallResult[]>([]);
|
|
33
|
+
|
|
34
|
+
useInput((input, key) => {
|
|
35
|
+
if (key.escape) {
|
|
36
|
+
if (view === "main") {
|
|
37
|
+
exit();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (input === "q") {
|
|
41
|
+
exit();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const handleToggle = (name: string) => {
|
|
46
|
+
const newSelected = new Set(selected);
|
|
47
|
+
if (newSelected.has(name)) {
|
|
48
|
+
newSelected.delete(name);
|
|
49
|
+
} else {
|
|
50
|
+
newSelected.add(name);
|
|
51
|
+
}
|
|
52
|
+
setSelected(newSelected);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleConfirm = () => {
|
|
56
|
+
if (selected.size > 0) {
|
|
57
|
+
setView("installing");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleComplete = (installResults: InstallResult[]) => {
|
|
62
|
+
setResults(installResults);
|
|
63
|
+
setView("done");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const mainMenuItems = [
|
|
67
|
+
{ label: "Browse by category", value: "browse" },
|
|
68
|
+
{ label: "Search hooks", value: "search" },
|
|
69
|
+
{ label: "Exit", value: "exit" },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const handleMainSelect = (item: { value: string }) => {
|
|
73
|
+
if (item.value === "exit") {
|
|
74
|
+
exit();
|
|
75
|
+
} else {
|
|
76
|
+
setView(item.value as View);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Box flexDirection="column" padding={1}>
|
|
82
|
+
<Header
|
|
83
|
+
title="Hooks"
|
|
84
|
+
subtitle="Install Claude Code hooks for your project"
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
{view === "main" && (
|
|
88
|
+
<Box flexDirection="column">
|
|
89
|
+
<Text marginBottom={1}>What would you like to do?</Text>
|
|
90
|
+
<SelectInput items={mainMenuItems} onSelect={handleMainSelect} />
|
|
91
|
+
<Box marginTop={1}>
|
|
92
|
+
<Text dimColor>Press q to quit</Text>
|
|
93
|
+
</Box>
|
|
94
|
+
</Box>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{view === "browse" && !category && (
|
|
98
|
+
<CategorySelect
|
|
99
|
+
onSelect={(cat) => {
|
|
100
|
+
setCategory(cat as Category);
|
|
101
|
+
setView("hooks");
|
|
102
|
+
}}
|
|
103
|
+
onBack={() => setView("main")}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{view === "hooks" && category && (
|
|
108
|
+
<HookSelect
|
|
109
|
+
hooks={getHooksByCategory(category)}
|
|
110
|
+
selected={selected}
|
|
111
|
+
onToggle={handleToggle}
|
|
112
|
+
onConfirm={handleConfirm}
|
|
113
|
+
onBack={() => {
|
|
114
|
+
setCategory(null);
|
|
115
|
+
setView("browse");
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{view === "search" && (
|
|
121
|
+
<SearchView
|
|
122
|
+
selected={selected}
|
|
123
|
+
onToggle={handleToggle}
|
|
124
|
+
onConfirm={handleConfirm}
|
|
125
|
+
onBack={() => setView("main")}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{view === "installing" && (
|
|
130
|
+
<InstallProgress
|
|
131
|
+
hooks={Array.from(selected)}
|
|
132
|
+
overwrite={overwrite}
|
|
133
|
+
onComplete={handleComplete}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{view === "done" && (
|
|
138
|
+
<Box flexDirection="column">
|
|
139
|
+
<Text bold color="green" marginBottom={1}>
|
|
140
|
+
Installation complete!
|
|
141
|
+
</Text>
|
|
142
|
+
|
|
143
|
+
{results.filter((r) => r.success).length > 0 && (
|
|
144
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
145
|
+
<Text bold>Installed:</Text>
|
|
146
|
+
{results
|
|
147
|
+
.filter((r) => r.success)
|
|
148
|
+
.map((r) => (
|
|
149
|
+
<Text key={r.hook} color="green">
|
|
150
|
+
{"\u2713"} {r.hook}
|
|
151
|
+
</Text>
|
|
152
|
+
))}
|
|
153
|
+
</Box>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{results.filter((r) => !r.success).length > 0 && (
|
|
157
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
158
|
+
<Text bold color="red">
|
|
159
|
+
Failed:
|
|
160
|
+
</Text>
|
|
161
|
+
{results
|
|
162
|
+
.filter((r) => !r.success)
|
|
163
|
+
.map((r) => (
|
|
164
|
+
<Text key={r.hook} color="red">
|
|
165
|
+
{"\u2717"} {r.hook}: {r.error}
|
|
166
|
+
</Text>
|
|
167
|
+
))}
|
|
168
|
+
</Box>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<Box marginTop={1} flexDirection="column">
|
|
172
|
+
<Text bold>What happened:</Text>
|
|
173
|
+
<Text>1. Hook source copied to .hooks/</Text>
|
|
174
|
+
<Text>2. Hook registered in ~/.claude/settings.json</Text>
|
|
175
|
+
<Text>3. Ready to use in Claude Code sessions</Text>
|
|
176
|
+
</Box>
|
|
177
|
+
|
|
178
|
+
<Box marginTop={1} flexDirection="column">
|
|
179
|
+
<Text bold>Next steps:</Text>
|
|
180
|
+
<Text dimColor> hooks list --registered Check active hooks</Text>
|
|
181
|
+
<Text dimColor> hooks info {"<name>"} View hook details</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
|
|
184
|
+
<Box marginTop={1}>
|
|
185
|
+
<Text dimColor>Press q to exit</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
</Box>
|
|
188
|
+
)}
|
|
189
|
+
</Box>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import SelectInput from "ink-select-input";
|
|
4
|
+
import { CATEGORIES, getHooksByCategory } from "../../lib/registry.js";
|
|
5
|
+
|
|
6
|
+
interface CategorySelectProps {
|
|
7
|
+
onSelect: (category: string) => void;
|
|
8
|
+
onBack?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CategorySelect({ onSelect, onBack }: CategorySelectProps) {
|
|
12
|
+
const items = CATEGORIES.map((cat) => ({
|
|
13
|
+
label: `${cat} (${getHooksByCategory(cat).length})`,
|
|
14
|
+
value: cat,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
if (onBack) {
|
|
18
|
+
items.unshift({ label: "\u2190 Back", value: "__back__" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handleSelect = (item: { value: string }) => {
|
|
22
|
+
if (item.value === "__back__" && onBack) {
|
|
23
|
+
onBack();
|
|
24
|
+
} else {
|
|
25
|
+
onSelect(item.value);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection="column">
|
|
31
|
+
<Text bold marginBottom={1}>
|
|
32
|
+
Select a category:
|
|
33
|
+
</Text>
|
|
34
|
+
<SelectInput items={items} onSelect={handleSelect} />
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { HookMeta } from "../../lib/registry.js";
|
|
4
|
+
|
|
5
|
+
const COL_CHECK = 5;
|
|
6
|
+
const COL_NAME = 18;
|
|
7
|
+
const COL_VERSION = 9;
|
|
8
|
+
const COL_EVENT = 14;
|
|
9
|
+
const COL_DESC = 40;
|
|
10
|
+
|
|
11
|
+
function truncate(str: string, max: number): string {
|
|
12
|
+
return str.length > max ? str.slice(0, max - 3) + "..." : str;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pad(str: string, width: number): string {
|
|
16
|
+
return str.length >= width ? str.slice(0, width) : str + " ".repeat(width - str.length);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DataTableProps {
|
|
20
|
+
hooks: HookMeta[];
|
|
21
|
+
selected: Set<string>;
|
|
22
|
+
onToggle: (name: string) => void;
|
|
23
|
+
onConfirm: () => void;
|
|
24
|
+
onBack: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DataTable({
|
|
28
|
+
hooks,
|
|
29
|
+
selected,
|
|
30
|
+
onToggle,
|
|
31
|
+
onConfirm,
|
|
32
|
+
onBack,
|
|
33
|
+
}: DataTableProps) {
|
|
34
|
+
const [cursor, setCursor] = useState(0);
|
|
35
|
+
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
if (key.upArrow) {
|
|
38
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
39
|
+
} else if (key.downArrow) {
|
|
40
|
+
setCursor((c) => Math.min(hooks.length - 1, c + 1));
|
|
41
|
+
} else if (key.return || input === " ") {
|
|
42
|
+
if (hooks[cursor]) {
|
|
43
|
+
onToggle(hooks[cursor].name);
|
|
44
|
+
}
|
|
45
|
+
} else if (input === "i" && selected.size > 0) {
|
|
46
|
+
onConfirm();
|
|
47
|
+
} else if (key.escape) {
|
|
48
|
+
onBack();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Box flexDirection="column">
|
|
54
|
+
<Box marginBottom={1}>
|
|
55
|
+
<Text bold>
|
|
56
|
+
Select hooks (up/down to navigate, space/enter to toggle, esc to go back):
|
|
57
|
+
</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
|
|
60
|
+
{/* Header */}
|
|
61
|
+
<Box>
|
|
62
|
+
<Text dimColor>
|
|
63
|
+
{pad("", COL_CHECK)}
|
|
64
|
+
{pad("Name", COL_NAME)}
|
|
65
|
+
{pad("Version", COL_VERSION)}
|
|
66
|
+
{pad("Event", COL_EVENT)}
|
|
67
|
+
{"Description"}
|
|
68
|
+
</Text>
|
|
69
|
+
</Box>
|
|
70
|
+
<Box marginBottom={0}>
|
|
71
|
+
<Text dimColor>
|
|
72
|
+
{pad("─".repeat(COL_CHECK - 1), COL_CHECK)}
|
|
73
|
+
{pad("─".repeat(COL_NAME - 1), COL_NAME)}
|
|
74
|
+
{pad("─".repeat(COL_VERSION - 1), COL_VERSION)}
|
|
75
|
+
{pad("─".repeat(COL_EVENT - 1), COL_EVENT)}
|
|
76
|
+
{"─".repeat(COL_DESC)}
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
|
|
80
|
+
{/* Rows */}
|
|
81
|
+
{hooks.map((hook, i) => {
|
|
82
|
+
const isHighlighted = i === cursor;
|
|
83
|
+
const isSelected = selected.has(hook.name);
|
|
84
|
+
const check = isSelected ? "[x]" : "[ ]";
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Box key={hook.name}>
|
|
88
|
+
<Text
|
|
89
|
+
bold={isHighlighted}
|
|
90
|
+
inverse={isHighlighted}
|
|
91
|
+
>
|
|
92
|
+
{pad(` ${check}`, COL_CHECK)}
|
|
93
|
+
{pad(hook.displayName, COL_NAME)}
|
|
94
|
+
{pad(hook.version, COL_VERSION)}
|
|
95
|
+
{pad(hook.event, COL_EVENT)}
|
|
96
|
+
{truncate(hook.description, COL_DESC)}
|
|
97
|
+
</Text>
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
|
|
102
|
+
{/* Footer */}
|
|
103
|
+
<Box marginTop={1} flexDirection="row" gap={2}>
|
|
104
|
+
<Text dimColor>[esc] Back</Text>
|
|
105
|
+
{selected.size > 0 && (
|
|
106
|
+
<Text color="green" bold>
|
|
107
|
+
{selected.size} selected
|
|
108
|
+
</Text>
|
|
109
|
+
)}
|
|
110
|
+
</Box>
|
|
111
|
+
|
|
112
|
+
{selected.size > 0 && (
|
|
113
|
+
<Box marginTop={0}>
|
|
114
|
+
<Text dimColor>
|
|
115
|
+
Selected: {Array.from(selected).join(", ")}
|
|
116
|
+
</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{selected.size > 0 && (
|
|
121
|
+
<Box marginTop={1}>
|
|
122
|
+
<Text>
|
|
123
|
+
Press{" "}
|
|
124
|
+
<Text bold color="cyan">
|
|
125
|
+
i
|
|
126
|
+
</Text>
|
|
127
|
+
{" "}to install selected ({selected.size})
|
|
128
|
+
</Text>
|
|
129
|
+
</Box>
|
|
130
|
+
)}
|
|
131
|
+
</Box>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
interface HeaderProps {
|
|
5
|
+
title?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Header({ title = "Hooks", subtitle }: HeaderProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
12
|
+
<Text bold color="cyan">
|
|
13
|
+
{title}
|
|
14
|
+
</Text>
|
|
15
|
+
{subtitle && <Text dimColor>{subtitle}</Text>}
|
|
16
|
+
</Box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { HookMeta } from "../../lib/registry.js";
|
|
3
|
+
import { DataTable } from "./DataTable.js";
|
|
4
|
+
|
|
5
|
+
interface HookSelectProps {
|
|
6
|
+
hooks: HookMeta[];
|
|
7
|
+
selected: Set<string>;
|
|
8
|
+
onToggle: (name: string) => void;
|
|
9
|
+
onConfirm: () => void;
|
|
10
|
+
onBack: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function HookSelect({
|
|
14
|
+
hooks,
|
|
15
|
+
selected,
|
|
16
|
+
onToggle,
|
|
17
|
+
onConfirm,
|
|
18
|
+
onBack,
|
|
19
|
+
}: HookSelectProps) {
|
|
20
|
+
return (
|
|
21
|
+
<DataTable
|
|
22
|
+
hooks={hooks}
|
|
23
|
+
selected={selected}
|
|
24
|
+
onToggle={onToggle}
|
|
25
|
+
onConfirm={onConfirm}
|
|
26
|
+
onBack={onBack}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { installHook, InstallResult } from "../../lib/installer.js";
|
|
5
|
+
import { getHook } from "../../lib/registry.js";
|
|
6
|
+
|
|
7
|
+
interface InstallProgressProps {
|
|
8
|
+
hooks: string[];
|
|
9
|
+
overwrite?: boolean;
|
|
10
|
+
onComplete: (results: InstallResult[]) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function InstallProgress({
|
|
14
|
+
hooks,
|
|
15
|
+
overwrite = false,
|
|
16
|
+
onComplete,
|
|
17
|
+
}: InstallProgressProps) {
|
|
18
|
+
const [results, setResults] = useState<InstallResult[]>([]);
|
|
19
|
+
const [current, setCurrent] = useState(0);
|
|
20
|
+
const [installing, setInstalling] = useState(true);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const install = async () => {
|
|
24
|
+
const newResults: InstallResult[] = [];
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
27
|
+
setCurrent(i);
|
|
28
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
29
|
+
|
|
30
|
+
const result = installHook(hooks[i], { overwrite });
|
|
31
|
+
newResults.push(result);
|
|
32
|
+
setResults([...newResults]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setInstalling(false);
|
|
36
|
+
onComplete(newResults);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
install();
|
|
40
|
+
}, [hooks, overwrite]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box flexDirection="column">
|
|
44
|
+
<Box marginBottom={1}>
|
|
45
|
+
<Text bold>
|
|
46
|
+
{installing
|
|
47
|
+
? `Installing hooks (${current + 1}/${hooks.length})...`
|
|
48
|
+
: "Installation complete!"}
|
|
49
|
+
</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
|
|
52
|
+
{hooks.map((name, i) => {
|
|
53
|
+
const result = results[i];
|
|
54
|
+
const isCurrent = i === current && installing;
|
|
55
|
+
const meta = getHook(name);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Box key={name} flexDirection="column">
|
|
59
|
+
{isCurrent && !result && (
|
|
60
|
+
<Text color="cyan">
|
|
61
|
+
<Spinner type="dots" /> {name}
|
|
62
|
+
</Text>
|
|
63
|
+
)}
|
|
64
|
+
{result?.success && (
|
|
65
|
+
<Box flexDirection="column">
|
|
66
|
+
<Text color="green">{"\u2713"} {name}</Text>
|
|
67
|
+
{meta && (
|
|
68
|
+
<Text dimColor>
|
|
69
|
+
{" "}{meta.event}{meta.matcher ? ` [${meta.matcher}]` : ""}
|
|
70
|
+
</Text>
|
|
71
|
+
)}
|
|
72
|
+
</Box>
|
|
73
|
+
)}
|
|
74
|
+
{result && !result.success && (
|
|
75
|
+
<Text color="red">
|
|
76
|
+
{"\u2717"} {name} - {result.error}
|
|
77
|
+
</Text>
|
|
78
|
+
)}
|
|
79
|
+
{!isCurrent && !result && (
|
|
80
|
+
<Text dimColor>{"\u25CB"} {name}</Text>
|
|
81
|
+
)}
|
|
82
|
+
</Box>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
|
|
86
|
+
{!installing && (
|
|
87
|
+
<Box marginTop={1} flexDirection="column">
|
|
88
|
+
<Text>
|
|
89
|
+
<Text color="green">
|
|
90
|
+
{results.filter((r) => r.success).length} installed
|
|
91
|
+
</Text>
|
|
92
|
+
{results.some((r) => !r.success) && (
|
|
93
|
+
<Text color="red">
|
|
94
|
+
, {results.filter((r) => !r.success).length} failed
|
|
95
|
+
</Text>
|
|
96
|
+
)}
|
|
97
|
+
</Text>
|
|
98
|
+
<Text dimColor>
|
|
99
|
+
Hooks installed to .hooks/ and registered in ~/.claude/settings.json
|
|
100
|
+
</Text>
|
|
101
|
+
</Box>
|
|
102
|
+
)}
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { searchHooks, HookMeta } from "../../lib/registry.js";
|
|
5
|
+
import { DataTable } from "./DataTable.js";
|
|
6
|
+
|
|
7
|
+
interface SearchViewProps {
|
|
8
|
+
selected: Set<string>;
|
|
9
|
+
onToggle: (name: string) => void;
|
|
10
|
+
onConfirm: () => void;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SearchView({
|
|
15
|
+
selected,
|
|
16
|
+
onToggle,
|
|
17
|
+
onConfirm,
|
|
18
|
+
onBack,
|
|
19
|
+
}: SearchViewProps) {
|
|
20
|
+
const [query, setQuery] = useState("");
|
|
21
|
+
const [results, setResults] = useState<HookMeta[]>([]);
|
|
22
|
+
const [mode, setMode] = useState<"search" | "select">("search");
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (query.length >= 2) {
|
|
26
|
+
setResults(searchHooks(query));
|
|
27
|
+
} else {
|
|
28
|
+
setResults([]);
|
|
29
|
+
}
|
|
30
|
+
}, [query]);
|
|
31
|
+
|
|
32
|
+
useInput((input, key) => {
|
|
33
|
+
if (key.escape) {
|
|
34
|
+
if (mode === "select") {
|
|
35
|
+
setMode("search");
|
|
36
|
+
} else {
|
|
37
|
+
onBack();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (key.downArrow && mode === "search" && results.length > 0) {
|
|
41
|
+
setMode("select");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box flexDirection="column">
|
|
47
|
+
<Box marginBottom={1}>
|
|
48
|
+
<Text bold>Search: </Text>
|
|
49
|
+
{mode === "search" && (
|
|
50
|
+
<TextInput
|
|
51
|
+
value={query}
|
|
52
|
+
onChange={setQuery}
|
|
53
|
+
placeholder="Type to search hooks..."
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
{mode === "select" && (
|
|
57
|
+
<Text>{query}</Text>
|
|
58
|
+
)}
|
|
59
|
+
</Box>
|
|
60
|
+
|
|
61
|
+
{query.length < 2 && (
|
|
62
|
+
<Text dimColor>Type at least 2 characters to search</Text>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{query.length >= 2 && results.length === 0 && (
|
|
66
|
+
<Text dimColor>No hooks found for "{query}"</Text>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{results.length > 0 && mode === "search" && (
|
|
70
|
+
<Text dimColor>
|
|
71
|
+
Found {results.length} hook(s) — press down arrow to browse results
|
|
72
|
+
</Text>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{results.length > 0 && mode === "select" && (
|
|
76
|
+
<DataTable
|
|
77
|
+
hooks={results}
|
|
78
|
+
selected={selected}
|
|
79
|
+
onToggle={onToggle}
|
|
80
|
+
onConfirm={onConfirm}
|
|
81
|
+
onBack={() => setMode("search")}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|