@goodfoot/claude-code-hooks 1.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/LICENSE +21 -0
- package/README.md +317 -0
- package/dist/cli.js +914 -0
- package/dist/constants.js +21 -0
- package/dist/env.js +188 -0
- package/dist/hooks.js +391 -0
- package/dist/index.js +77 -0
- package/dist/inputs.js +35 -0
- package/dist/logger.js +494 -0
- package/dist/outputs.js +282 -0
- package/dist/runtime.js +222 -0
- package/dist/scaffold.js +466 -0
- package/dist/tool-helpers.js +366 -0
- package/dist/tool-inputs.js +21 -0
- package/package.json +68 -0
- package/types/cli.d.ts +281 -0
- package/types/constants.d.ts +9 -0
- package/types/env.d.ts +150 -0
- package/types/hooks.d.ts +851 -0
- package/types/index.d.ts +137 -0
- package/types/inputs.d.ts +601 -0
- package/types/logger.d.ts +471 -0
- package/types/outputs.d.ts +643 -0
- package/types/runtime.d.ts +75 -0
- package/types/scaffold.d.ts +46 -0
- package/types/tool-helpers.d.ts +336 -0
- package/types/tool-inputs.d.ts +228 -0
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold module for generating new Claude Code hook projects.
|
|
3
|
+
*
|
|
4
|
+
* Generates a complete TypeScript project structure with:
|
|
5
|
+
* - package.json with dependencies and scripts
|
|
6
|
+
* - tsconfig.json with ESM/Node20 configuration
|
|
7
|
+
* - biome.json for linting/formatting
|
|
8
|
+
* - Hook template files for each requested hook type
|
|
9
|
+
* - Vitest test files for each hook
|
|
10
|
+
* - vitest.config.ts for test configuration
|
|
11
|
+
* @module
|
|
12
|
+
* @example
|
|
13
|
+
* ```bash
|
|
14
|
+
* npx @goodfoot/claude-code-hooks --scaffold ./my-hooks --hooks Stop,SubagentStop -o dist/hooks.json
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { HOOK_FACTORY_TO_EVENT } from './constants.js';
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Constants
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Valid hook event names (derived from HOOK_FACTORY_TO_EVENT values).
|
|
25
|
+
*/
|
|
26
|
+
const VALID_HOOK_EVENT_NAMES = new Set(Object.values(HOOK_FACTORY_TO_EVENT));
|
|
27
|
+
/**
|
|
28
|
+
* Case-insensitive lookup map for hook event names.
|
|
29
|
+
* Built once at module load to avoid recreation on each validation call.
|
|
30
|
+
*/
|
|
31
|
+
const CASE_INSENSITIVE_EVENT_LOOKUP = new Map(
|
|
32
|
+
Array.from(VALID_HOOK_EVENT_NAMES).map((name) => [name.toLowerCase(), name])
|
|
33
|
+
);
|
|
34
|
+
/**
|
|
35
|
+
* Mapping from hook event name to factory function name.
|
|
36
|
+
*/
|
|
37
|
+
const EVENT_TO_HOOK_FACTORY = Object.fromEntries(
|
|
38
|
+
Object.entries(HOOK_FACTORY_TO_EVENT).map(([factory, event]) => [event, factory])
|
|
39
|
+
);
|
|
40
|
+
/**
|
|
41
|
+
* Mapping from hook event name to output function name.
|
|
42
|
+
*/
|
|
43
|
+
const EVENT_TO_OUTPUT_FUNCTION = {
|
|
44
|
+
PreToolUse: 'preToolUseOutput',
|
|
45
|
+
PostToolUse: 'postToolUseOutput',
|
|
46
|
+
PostToolUseFailure: 'postToolUseFailureOutput',
|
|
47
|
+
Notification: 'notificationOutput',
|
|
48
|
+
UserPromptSubmit: 'userPromptSubmitOutput',
|
|
49
|
+
SessionStart: 'sessionStartOutput',
|
|
50
|
+
SessionEnd: 'sessionEndOutput',
|
|
51
|
+
Stop: 'stopOutput',
|
|
52
|
+
SubagentStart: 'subagentStartOutput',
|
|
53
|
+
SubagentStop: 'subagentStopOutput',
|
|
54
|
+
PreCompact: 'preCompactOutput',
|
|
55
|
+
PermissionRequest: 'permissionRequestOutput'
|
|
56
|
+
};
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Validation
|
|
59
|
+
// ============================================================================
|
|
60
|
+
/**
|
|
61
|
+
* Validates hook names against valid hook event names.
|
|
62
|
+
*
|
|
63
|
+
* Accepts PascalCase names case-insensitively (e.g., 'stop' -> 'Stop').
|
|
64
|
+
* @param hookNames - Array of hook names to validate
|
|
65
|
+
* @returns Object with normalized hook names or error message
|
|
66
|
+
*/
|
|
67
|
+
function validateHookNames(hookNames) {
|
|
68
|
+
const normalized = [];
|
|
69
|
+
const invalid = [];
|
|
70
|
+
for (const hookName of hookNames) {
|
|
71
|
+
const normalizedName = CASE_INSENSITIVE_EVENT_LOOKUP.get(hookName.toLowerCase());
|
|
72
|
+
if (normalizedName !== undefined) {
|
|
73
|
+
normalized.push(normalizedName);
|
|
74
|
+
} else {
|
|
75
|
+
invalid.push(hookName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (invalid.length > 0) {
|
|
79
|
+
const validNames = Array.from(VALID_HOOK_EVENT_NAMES).sort().join(', ');
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
error: `Invalid hook name(s): ${invalid.join(', ')}\nValid hook names: ${validNames}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { valid: true, normalized };
|
|
86
|
+
}
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// File Generation
|
|
89
|
+
// ============================================================================
|
|
90
|
+
/**
|
|
91
|
+
* Converts a PascalCase hook event name to kebab-case filename.
|
|
92
|
+
* @param eventName - Hook event name (e.g., 'PreToolUse')
|
|
93
|
+
* @returns Kebab-case filename (e.g., 'pre-tool-use')
|
|
94
|
+
*/
|
|
95
|
+
function toKebabCase(eventName) {
|
|
96
|
+
return eventName
|
|
97
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
98
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generates package.json content for the scaffolded project.
|
|
103
|
+
* @param projectName - Name derived from directory basename
|
|
104
|
+
* @param outputPath - Relative path for hooks.json output
|
|
105
|
+
* @returns JSON string for package.json
|
|
106
|
+
*/
|
|
107
|
+
function generatePackageJson(projectName, outputPath) {
|
|
108
|
+
const packageJson = {
|
|
109
|
+
name: projectName,
|
|
110
|
+
version: '1.0.0',
|
|
111
|
+
type: 'module',
|
|
112
|
+
scripts: {
|
|
113
|
+
build: `claude-code-hooks -i "src/**/*.ts" -o "${outputPath}"`,
|
|
114
|
+
test: 'vitest run',
|
|
115
|
+
lint: 'biome check .',
|
|
116
|
+
typecheck: 'tsc --noEmit'
|
|
117
|
+
},
|
|
118
|
+
dependencies: {
|
|
119
|
+
'@goodfoot/claude-code-hooks': '^1.0.0'
|
|
120
|
+
},
|
|
121
|
+
devDependencies: {
|
|
122
|
+
'@biomejs/biome': '^1.9.0',
|
|
123
|
+
'@types/node': '^20.0.0',
|
|
124
|
+
typescript: '^5.6.0',
|
|
125
|
+
vitest: '^2.0.0'
|
|
126
|
+
},
|
|
127
|
+
engines: {
|
|
128
|
+
node: '>=20.11.0'
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
return JSON.stringify(packageJson, null, 2) + '\n';
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generates tsconfig.json content for the scaffolded project.
|
|
135
|
+
* @returns JSON string for tsconfig.json
|
|
136
|
+
*/
|
|
137
|
+
function generateTsConfig() {
|
|
138
|
+
const tsconfig = {
|
|
139
|
+
compilerOptions: {
|
|
140
|
+
target: 'ES2022',
|
|
141
|
+
module: 'NodeNext',
|
|
142
|
+
moduleResolution: 'NodeNext',
|
|
143
|
+
strict: true,
|
|
144
|
+
esModuleInterop: true,
|
|
145
|
+
skipLibCheck: true,
|
|
146
|
+
declaration: true,
|
|
147
|
+
declarationMap: true,
|
|
148
|
+
outDir: './dist',
|
|
149
|
+
rootDir: './src'
|
|
150
|
+
},
|
|
151
|
+
include: ['src/**/*.ts'],
|
|
152
|
+
exclude: ['node_modules', 'dist', 'test']
|
|
153
|
+
};
|
|
154
|
+
return JSON.stringify(tsconfig, null, 2) + '\n';
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Generates biome.json content for the scaffolded project.
|
|
158
|
+
*
|
|
159
|
+
* The ignore array is formatted on one line to match biome's own formatting preferences.
|
|
160
|
+
* This avoids lint errors when running `biome check` on the generated project.
|
|
161
|
+
* @returns JSON string for biome.json
|
|
162
|
+
*/
|
|
163
|
+
function generateBiomeConfig() {
|
|
164
|
+
// Generate JSON manually to ensure ignore array is on one line
|
|
165
|
+
// (biome's formatter expects this format)
|
|
166
|
+
return `{
|
|
167
|
+
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
|
168
|
+
"organizeImports": {
|
|
169
|
+
"enabled": true
|
|
170
|
+
},
|
|
171
|
+
"formatter": {
|
|
172
|
+
"enabled": true,
|
|
173
|
+
"indentStyle": "space",
|
|
174
|
+
"indentWidth": 2,
|
|
175
|
+
"lineWidth": 120
|
|
176
|
+
},
|
|
177
|
+
"linter": {
|
|
178
|
+
"enabled": true,
|
|
179
|
+
"rules": {
|
|
180
|
+
"recommended": true
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"files": {
|
|
184
|
+
"ignore": ["node_modules", "dist", "build", "*.json"]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generates vitest.config.ts content for the scaffolded project.
|
|
191
|
+
*
|
|
192
|
+
* Uses double quotes and trailing commas to match biome's formatting preferences.
|
|
193
|
+
* @returns TypeScript content for vitest.config.ts
|
|
194
|
+
*/
|
|
195
|
+
function generateVitestConfig() {
|
|
196
|
+
return `import { defineConfig } from "vitest/config";
|
|
197
|
+
|
|
198
|
+
export default defineConfig({
|
|
199
|
+
test: {
|
|
200
|
+
include: ["test/**/*.test.ts"],
|
|
201
|
+
globals: false,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Generates CLAUDE.md content for the scaffolded project.
|
|
208
|
+
* @returns Markdown content for CLAUDE.md
|
|
209
|
+
*/
|
|
210
|
+
function generateClaudeMd() {
|
|
211
|
+
return 'Load the `claude-code-hooks:claude-code-hooks` skill immediately if it is available.\n';
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Generates .gitignore content for the scaffolded project.
|
|
215
|
+
* @returns Content for .gitignore
|
|
216
|
+
*/
|
|
217
|
+
function generateGitignore() {
|
|
218
|
+
return `# Dependencies
|
|
219
|
+
node_modules/
|
|
220
|
+
|
|
221
|
+
# Build outputs
|
|
222
|
+
dist/
|
|
223
|
+
build/
|
|
224
|
+
*.mjs
|
|
225
|
+
|
|
226
|
+
# Generated files
|
|
227
|
+
hooks.json
|
|
228
|
+
|
|
229
|
+
# IDE
|
|
230
|
+
.idea/
|
|
231
|
+
.vscode/
|
|
232
|
+
*.swp
|
|
233
|
+
*.swo
|
|
234
|
+
|
|
235
|
+
# OS
|
|
236
|
+
.DS_Store
|
|
237
|
+
Thumbs.db
|
|
238
|
+
|
|
239
|
+
# Logs
|
|
240
|
+
*.log
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Generates README.md content for the scaffolded project.
|
|
245
|
+
* @param projectName - Name of the project
|
|
246
|
+
* @param hooks - Array of hook event names included in this project
|
|
247
|
+
* @returns Markdown content for README.md
|
|
248
|
+
*/
|
|
249
|
+
function generateReadme(projectName, hooks) {
|
|
250
|
+
const hookList = hooks.map((h) => `\`${h}\``).join(', ');
|
|
251
|
+
return `# ${projectName}
|
|
252
|
+
|
|
253
|
+
This project contains [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) built with the \`@goodfoot/claude-code-hooks\` library. Hooks let you extend Claude Code's behavior by running custom code at specific points during a session—before or after tool execution, when Claude starts or stops, and more. This project includes hooks for: ${hookList}.
|
|
254
|
+
|
|
255
|
+
To get started, run \`npm install\` to install dependencies, then \`npm run build\` to compile your hooks into \`hooks.json\`. Copy the generated \`hooks.json\` to your Claude Code settings directory (or reference it in your \`.claude/settings.json\`), and your hooks will run automatically. Edit the files in \`src/\` to customize behavior, and use \`npm test\` to verify your changes work correctly.
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Generates a hook template file for a specific hook type.
|
|
260
|
+
*
|
|
261
|
+
* Uses double quotes for all strings to match biome's formatting preferences.
|
|
262
|
+
* @param eventName - Hook event name (e.g., 'Stop')
|
|
263
|
+
* @returns TypeScript content for the hook file
|
|
264
|
+
*/
|
|
265
|
+
function generateHookTemplate(eventName) {
|
|
266
|
+
const factoryName = EVENT_TO_HOOK_FACTORY[eventName];
|
|
267
|
+
const outputName = EVENT_TO_OUTPUT_FUNCTION[eventName];
|
|
268
|
+
// Generate appropriate return statement based on hook type
|
|
269
|
+
// All hooks include systemMessage to demonstrate the pattern
|
|
270
|
+
let returnStatement;
|
|
271
|
+
switch (eventName) {
|
|
272
|
+
case 'Stop':
|
|
273
|
+
case 'SubagentStop':
|
|
274
|
+
returnStatement = `return ${outputName}({
|
|
275
|
+
decision: "approve",
|
|
276
|
+
systemMessage: "${eventName} hook executed successfully.",
|
|
277
|
+
});`;
|
|
278
|
+
break;
|
|
279
|
+
case 'PreToolUse':
|
|
280
|
+
returnStatement = `return ${outputName}({
|
|
281
|
+
systemMessage: "Tool execution allowed by ${eventName} hook.",
|
|
282
|
+
hookSpecificOutput: { permissionDecision: "allow" },
|
|
283
|
+
});`;
|
|
284
|
+
break;
|
|
285
|
+
case 'PostToolUse':
|
|
286
|
+
case 'PostToolUseFailure':
|
|
287
|
+
returnStatement = `return ${outputName}({
|
|
288
|
+
systemMessage: "${eventName} hook processed the result.",
|
|
289
|
+
hookSpecificOutput: { additionalContext: "Hook completed successfully." },
|
|
290
|
+
});`;
|
|
291
|
+
break;
|
|
292
|
+
case 'SessionStart':
|
|
293
|
+
returnStatement = `return ${outputName}({
|
|
294
|
+
systemMessage: "Session initialized by ${eventName} hook.",
|
|
295
|
+
hookSpecificOutput: { additionalContext: "Environment ready." },
|
|
296
|
+
});`;
|
|
297
|
+
break;
|
|
298
|
+
case 'UserPromptSubmit':
|
|
299
|
+
returnStatement = `return ${outputName}({
|
|
300
|
+
systemMessage: "User prompt received.",
|
|
301
|
+
hookSpecificOutput: { additionalContext: "Prompt processed by hook." },
|
|
302
|
+
});`;
|
|
303
|
+
break;
|
|
304
|
+
case 'PreCompact':
|
|
305
|
+
returnStatement = `return ${outputName}({
|
|
306
|
+
systemMessage: "Remember: Context is being compacted.",
|
|
307
|
+
});`;
|
|
308
|
+
break;
|
|
309
|
+
case 'PermissionRequest':
|
|
310
|
+
returnStatement = `return ${outputName}({
|
|
311
|
+
systemMessage: "Permission request processed.",
|
|
312
|
+
});`;
|
|
313
|
+
break;
|
|
314
|
+
default:
|
|
315
|
+
// SessionEnd, Notification, SubagentStart use simple output with systemMessage
|
|
316
|
+
returnStatement = `return ${outputName}({
|
|
317
|
+
systemMessage: "${eventName} hook executed.",
|
|
318
|
+
});`;
|
|
319
|
+
}
|
|
320
|
+
// SessionStart hooks get extended context with persistEnvVar
|
|
321
|
+
const contextDestructure = eventName === 'SessionStart' ? '{ logger, persistEnvVar }' : '{ logger }';
|
|
322
|
+
return `/**
|
|
323
|
+
* ${eventName} hook implementation.
|
|
324
|
+
*
|
|
325
|
+
* @see https://code.claude.com/docs/en/hooks#${eventName.toLowerCase()}
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
import { ${factoryName}, ${outputName} } from "@goodfoot/claude-code-hooks";
|
|
329
|
+
|
|
330
|
+
export default ${factoryName}({}, (input, ${contextDestructure}) => {
|
|
331
|
+
logger.info("${eventName} hook triggered", { input });
|
|
332
|
+
${returnStatement}
|
|
333
|
+
});
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Generates a test file for a specific hook type.
|
|
338
|
+
*
|
|
339
|
+
* Uses double quotes, alphabetical import order (describe, expect, it),
|
|
340
|
+
* and trailing commas to match biome's formatting preferences.
|
|
341
|
+
* Uses the real Logger class (silent by default) instead of mocks.
|
|
342
|
+
* @param eventName - Hook event name (e.g., 'Stop')
|
|
343
|
+
* @param hookFilename - Kebab-case filename of the hook (e.g., 'stop')
|
|
344
|
+
* @returns TypeScript content for the test file
|
|
345
|
+
*/
|
|
346
|
+
function generateTestFile(eventName, hookFilename) {
|
|
347
|
+
// SessionStart hooks need extended context with persistEnvVar
|
|
348
|
+
const contextCode =
|
|
349
|
+
eventName === 'SessionStart'
|
|
350
|
+
? `{
|
|
351
|
+
logger,
|
|
352
|
+
persistEnvVar: () => {},
|
|
353
|
+
persistEnvVars: () => {},
|
|
354
|
+
}`
|
|
355
|
+
: '{ logger }';
|
|
356
|
+
return `/**
|
|
357
|
+
* Tests for the ${eventName} hook.
|
|
358
|
+
*/
|
|
359
|
+
|
|
360
|
+
import { describe, expect, it } from "vitest";
|
|
361
|
+
import { Logger } from "@goodfoot/claude-code-hooks";
|
|
362
|
+
import hook from "../src/${hookFilename}.js";
|
|
363
|
+
|
|
364
|
+
// Logger is silent by default (no stdout/stderr output) — no mocking needed
|
|
365
|
+
const logger = new Logger();
|
|
366
|
+
|
|
367
|
+
describe("${eventName} Hook", () => {
|
|
368
|
+
it("exports a valid hook function", () => {
|
|
369
|
+
expect(hook).toBeDefined();
|
|
370
|
+
expect(typeof hook).toBe("function");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("has correct hookEventName metadata", () => {
|
|
374
|
+
expect(hook.hookEventName).toBe("${eventName}");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("returns a valid output shape", async () => {
|
|
378
|
+
const mockInput = {} as Parameters<typeof hook>[0];
|
|
379
|
+
const context = ${contextCode};
|
|
380
|
+
|
|
381
|
+
const result = await hook(mockInput, context);
|
|
382
|
+
|
|
383
|
+
// Verify output has expected structure
|
|
384
|
+
expect(result).toBeDefined();
|
|
385
|
+
expect(result).toHaveProperty("_type", "${eventName}");
|
|
386
|
+
expect(result).toHaveProperty("stdout");
|
|
387
|
+
expect(typeof result.stdout).toBe("object");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
// ============================================================================
|
|
393
|
+
// Main Scaffold Function
|
|
394
|
+
// ============================================================================
|
|
395
|
+
/**
|
|
396
|
+
* Scaffolds a new Claude Code hook project.
|
|
397
|
+
*
|
|
398
|
+
* Creates the complete project structure including:
|
|
399
|
+
* - package.json, tsconfig.json, biome.json, vitest.config.ts
|
|
400
|
+
* - src/ directory with hook implementations
|
|
401
|
+
* - test/ directory with vitest tests
|
|
402
|
+
* @param options - Scaffold configuration options
|
|
403
|
+
* @throws Exits with code 1 if directory exists or hook names are invalid
|
|
404
|
+
* @example
|
|
405
|
+
* ```typescript
|
|
406
|
+
* scaffoldProject({
|
|
407
|
+
* directory: './my-hooks',
|
|
408
|
+
* hooks: ['Stop', 'SubagentStop'],
|
|
409
|
+
* outputPath: 'dist/hooks.json'
|
|
410
|
+
* });
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
export function scaffoldProject(options) {
|
|
414
|
+
const { directory, hooks, outputPath } = options;
|
|
415
|
+
// Resolve to absolute path
|
|
416
|
+
const absoluteDir = path.resolve(process.cwd(), directory);
|
|
417
|
+
// Check if directory already exists
|
|
418
|
+
if (fs.existsSync(absoluteDir)) {
|
|
419
|
+
process.stderr.write(`Error: Directory already exists: ${absoluteDir}\n`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
// Validate hook names
|
|
423
|
+
const validation = validateHookNames(hooks);
|
|
424
|
+
if (!validation.valid) {
|
|
425
|
+
process.stderr.write(`Error: ${validation.error}\n`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
const normalizedHooks = validation.normalized;
|
|
429
|
+
// Create directory structure
|
|
430
|
+
const srcDir = path.join(absoluteDir, 'src');
|
|
431
|
+
const testDir = path.join(absoluteDir, 'test');
|
|
432
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
433
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
434
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
435
|
+
// Generate project name from directory basename
|
|
436
|
+
const projectName = path.basename(absoluteDir);
|
|
437
|
+
// Generate configuration files
|
|
438
|
+
fs.writeFileSync(path.join(absoluteDir, 'package.json'), generatePackageJson(projectName, outputPath));
|
|
439
|
+
fs.writeFileSync(path.join(absoluteDir, 'tsconfig.json'), generateTsConfig());
|
|
440
|
+
fs.writeFileSync(path.join(absoluteDir, 'biome.json'), generateBiomeConfig());
|
|
441
|
+
fs.writeFileSync(path.join(absoluteDir, 'vitest.config.ts'), generateVitestConfig());
|
|
442
|
+
fs.writeFileSync(path.join(absoluteDir, 'CLAUDE.md'), generateClaudeMd());
|
|
443
|
+
fs.writeFileSync(path.join(absoluteDir, '.gitignore'), generateGitignore());
|
|
444
|
+
fs.writeFileSync(path.join(absoluteDir, 'README.md'), generateReadme(projectName, normalizedHooks));
|
|
445
|
+
// Generate hook files and tests
|
|
446
|
+
for (const eventName of normalizedHooks) {
|
|
447
|
+
const kebabName = toKebabCase(eventName);
|
|
448
|
+
// Generate hook file
|
|
449
|
+
const hookContent = generateHookTemplate(eventName);
|
|
450
|
+
fs.writeFileSync(path.join(srcDir, `${kebabName}.ts`), hookContent);
|
|
451
|
+
// Generate test file
|
|
452
|
+
const testContent = generateTestFile(eventName, kebabName);
|
|
453
|
+
fs.writeFileSync(path.join(testDir, `${kebabName}.test.ts`), testContent);
|
|
454
|
+
}
|
|
455
|
+
// Output success message
|
|
456
|
+
process.stdout.write(`Created hook project at ${absoluteDir}\n`);
|
|
457
|
+
process.stdout.write('\nNext steps:\n');
|
|
458
|
+
process.stdout.write(` cd ${directory}\n`);
|
|
459
|
+
process.stdout.write(' npm install\n');
|
|
460
|
+
process.stdout.write(' npm run build\n');
|
|
461
|
+
process.stdout.write('\nGenerated hooks:\n');
|
|
462
|
+
for (const eventName of normalizedHooks) {
|
|
463
|
+
const kebabName = toKebabCase(eventName);
|
|
464
|
+
process.stdout.write(` - src/${kebabName}.ts\n`);
|
|
465
|
+
}
|
|
466
|
+
}
|