Hooks
Hooks allow custom logic at different stages of the installation process.
Basic Usage
Section titled “Basic Usage”import { defineTool } from "@alexgorbatchev/dotfiles";
export default defineTool((install, ctx) => install("github-release", { repo: "owner/tool" }) .bin("tool") .hook("after-install", async (context) => { const { $, log, fileSystem } = context; await $`./tool init`; log.info("Tool initialized"); }),);Hook Events
Section titled “Hook Events”| Event | When | Available Properties |
|---|---|---|
before-install | Before installation starts | stagingDir |
after-download | After file download | stagingDir, downloadPath |
after-extract | After archive extraction | stagingDir, downloadPath, extractDir |
after-install | After installation completes | installedDir, binaryPaths, version |
Context Properties
Section titled “Context Properties”All hooks receive a context object with:
| Property | Description |
|---|---|
toolName | Name of the tool |
currentDir | Stable path (symlink) for this tool |
stagingDir | Temporary installation directory |
systemInfo | Platform, architecture, home directory |
fileSystem | File operations (mkdir, writeFile, exists, etc.) |
replaceInFile | Regex-based file text replacement |
log | Structured logging (trace, debug, info, warn, error) |
projectConfig | Project configuration |
toolConfig | Tool configuration |
$ | Bun shell executor |
Note: The
stagingDirandprojectConfigproperties form the base environment context (IEnvContext) that is also available to dynamicenvfunctions in install parameters.
For archive-based installers, extractDir is a dedicated subdirectory under stagingDir so extracted payloads do not collide with generated binary entrypoints created at the staging root.
Examples
Section titled “Examples”File Operations
Section titled “File Operations”.hook('after-install', async ({ fileSystem, systemInfo, log }) => { const configDir = `${systemInfo.homeDir}/.config/tool`; await fileSystem.mkdir(configDir, { recursive: true }); await fileSystem.writeFile(`${configDir}/config.toml`, 'theme = "dark"'); log.info('Configuration created');})Shell Commands
Section titled “Shell Commands”.hook('after-install', async ({ $, installedDir }) => { // Run tool command await $`${installedDir}/tool init`;
// Capture output const version = await $`./tool --version`.text();})Executing Installed Binaries by Name
Section titled “Executing Installed Binaries by Name”In after-install hooks, the shell’s PATH is automatically enhanced to include the directories containing the installed binaries. This means you can execute freshly installed tools by name without specifying the full path:
.hook('after-install', async ({ $ }) => { // The installed binary is automatically available by name await $`my-tool --version`;
// No need to use full paths like: // await $`${installedDir}/bin/my-tool --version`;})This PATH enhancement only applies to after-install hooks where binaryPaths is available in the context.
Shell Command Logging
Section titled “Shell Command Logging”Shell commands executed in hooks are automatically logged to help with debugging and visibility:
- Commands are logged as
$ commandat info level before execution - Stdout lines are logged as
| lineat info level - Stderr lines are logged as
| lineat error level (only if stderr has content)
Example output:
$ my-tool init| Initializing configuration...| Configuration complete!This logging happens regardless of whether .quiet() is used on the shell command, since logging occurs at the hook executor level.
Platform-Specific Setup
Section titled “Platform-Specific Setup”.hook('after-install', async ({ systemInfo, $ }) => { if (systemInfo.platform === 'darwin') { await $`./setup-macos.sh`; } else if (systemInfo.platform === 'linux') { await $`./setup-linux.sh`; }})File Text Replacement
Section titled “File Text Replacement”.hook('after-install', async ({ replaceInFile, installedDir }) => { // Replace a config value (returns true if replaced, false otherwise) const wasReplaced = await replaceInFile( `${installedDir}/config.toml`, /theme = ".*"/, 'theme = "dark"' );
// Increment version numbers line-by-line await replaceInFile( `${installedDir}/versions.txt`, /version=(\d+)/, (match) => `version=${Number(match.captures[0]) + 1}`, { mode: 'line' } );
// Log error if pattern not found (helpful for debugging) await replaceInFile( `${installedDir}/config.toml`, /api_key = ".*"/, 'api_key = "secret"', { errorMessage: 'Could not find api_key setting' } );})Build from Source
Section titled “Build from Source”.hook('after-extract', async ({ extractDir, stagingDir, $ }) => { if (extractDir) { await $`cd ${extractDir} && make build`; await $`mv ${extractDir}/target/release/tool ${stagingDir}/tool`; }})Error Handling
Section titled “Error Handling”.hook('after-install', async ({ $, log }) => { try { await $`./tool self-test`; } catch (error) { log.error('Self-test failed'); throw error; // Re-throw to fail installation }});Custom Binary Processing
Section titled “Custom Binary Processing”import { defineTool } from "@alexgorbatchev/dotfiles";import path from "path";
export default defineTool((install, ctx) => install("github-release", { repo: "owner/custom-tool" }) .bin("custom-tool") .hook("after-extract", async ({ extractDir, stagingDir, fileSystem, log }) => { if (extractDir) { // Custom binary selection and processing const binaries = await fileSystem.readdir(path.join(extractDir, "bin")); const mainBinary = binaries.find((name) => name.startsWith("main-"));
if (mainBinary) { const sourcePath = path.join(extractDir, "bin", mainBinary); const targetPath = path.join(stagingDir ?? "", "tool"); await fileSystem.copy(sourcePath, targetPath); log.info(`Selected binary: ${mainBinary}`); } } }),);Environment-Specific Setup
Section titled “Environment-Specific Setup”import { defineTool } from "@alexgorbatchev/dotfiles";
export default defineTool((install, ctx) => install("github-release", { repo: "owner/custom-tool" }) .bin("custom-tool") .hook("after-install", async ({ systemInfo, fileSystem, log, $ }) => { // Platform-specific setup if (systemInfo.platform === "darwin") { // macOS-specific setup await $`./setup-macos.sh`; } else if (systemInfo.platform === "linux") { // Linux-specific setup await $`./setup-linux.sh`; }
// Architecture-specific setup if (systemInfo.arch === "arm64") { log.info("Configuring for ARM64 architecture"); await $`./configure-arm64.sh`; } }),);Environment Variables in Installation
Section titled “Environment Variables in Installation”Set environment variables during installation (for curl-script installs):
import { defineTool } from "@alexgorbatchev/dotfiles";
export default defineTool((install) => install("curl-script", { url: "https://example.com/install.sh", shell: "bash", env: { INSTALL_DIR: "~/.local/bin", ENABLE_FEATURE: "true", API_KEY: process.env.TOOL_API_KEY || "default", }, }).bin("my-tool"),);Best Practices
Section titled “Best Practices”- Use
$for shell operations that need to work with files relative to your tool config - Use
fileSystemmethods for cross-platform file operations that don’t require shell features - Always handle errors appropriately in hooks to provide clear feedback
- Use
logfor all output - avoidconsole.log()in favor of structured logging:log.info()for general informationlog.warn()for warningslog.error()for error conditionslog.debug()for debugging and troubleshooting
- Test your hooks on different platforms to ensure compatibility
- Keep hooks focused - each hook should have a single responsibility
- Document complex logic - explain what your hooks are doing and why
Hook Execution Order
Section titled “Hook Execution Order”beforeInstall: Before any installation stepsafterDownload: After downloading but before extractionafterExtract: After extraction but before binary setupafterInstall: After all installation steps are complete
Complete Example
Section titled “Complete Example”import { defineTool } from "@alexgorbatchev/dotfiles";import path from "path";
export default defineTool((install, ctx) => install("github-release", { repo: "owner/custom-tool" }) .bin("custom-tool") .symlink("./config.yml", "~/.config/custom-tool/config.yml") .hook("before-install", async ({ log }) => { log.info("Starting custom-tool installation..."); }) .hook("after-extract", async ({ extractDir, log, $ }) => { if (extractDir) { // Build additional components log.info("Building plugins..."); await $`cd ${extractDir} && make plugins`; } }) .hook("after-install", async ({ toolName, installedDir, systemInfo, fileSystem, log, $ }) => { // Create data directory const dataDir = path.join(systemInfo.homeDir, ".local/share", toolName); await fileSystem.mkdir(dataDir, { recursive: true });
// Initialize tool await $`${path.join(installedDir ?? "", toolName)} init --data-dir ${dataDir}`;
// Set up completion await $`${path.join( installedDir ?? "", toolName, )} completion zsh > ${ctx.projectConfig.paths.generatedDir}/completions/_${toolName}`;
log.info(`Initialized ${toolName} with data directory: ${dataDir}`); }) .zsh((shell) => shell.env({ CUSTOM_TOOL_DATA: "~/.local/share/custom-tool" }).aliases({ ct: "custom-tool" })),);